Skip to content

Sensor System

Concrete dist/claude/ paths in this chapter refer to the ignored local projection materialized by bun scripts/package.ts; installed projects use the same relative paths under .claude/.

Audience: Tier 2/3 (team adopter, framework contributor).

This chapter is the schema reference for AI-DLC sensor manifests — the deterministic checks that fire on writes to a stage's outputs. Sensors are the feedback half of the control loop; rules are the feedforward half (see Rule System, the next chapter). The Plane Architecture chapter frames both as control-plane inputs that the compile resolves into each stage node.

This chapter covers the manifest file format — what a sensor manifest contains, how stages import sensors, and how the six shipped manifests are configured. For the user-facing view of how sensors fire during a workflow, see Rules and the Learning Loop in the User Guide.

Path convention. <record>/ below = the active intent's record dir, aidlc/spaces/<space>/intents/<YYMMDD>-<label>/ (a compact UTC date prefix plus a short kebab-case label, so record dirs sort chronologically; the canonical id is the UUIDv7 stored in the intents.json registry row). Note the two document-shape sensors' matches glob in the shipped manifests still carries the legacy artifact-tree path (quoted verbatim below where the schema is documented).

For runtime behaviour see Stage Protocol. The file-format parallel for stage definitions lives at Stage Definition.


Manifest location and filename

Sensor manifests live at:

dist/claude/.claude/sensors/aidlc-<id>.md

Every framework-shipped manifest carries the aidlc- filename prefix (matching the broader framework-file convention). The frontmatter id: field MUST equal the filename stem with the aidlc- prefix removed and the .md suffix stripped:

Filename Required id:
aidlc-required-sections.md required-sections
aidlc-linter.md linter

The filename↔id rule is enforced by tests/unit/t86-sensor-manifest-schema.sh. The aidlc- prefix is mandatory for all sensors, including custom user-shipped ones: the compile resolver discovers manifests with SENSOR_FILE_REGEX = /^aidlc-([a-z][a-z0-9-]*)\.md$/ (loadSensors in aidlc-graph.ts), so any file without the prefix is silently skipped and never binds to a stage. Name a custom sensor aidlc-<id>.md and set id: <id>.


Sensor Manifest Schema

Every manifest is a Markdown file with YAML frontmatter and a body. The frontmatter is the structured contract — a pure capability descriptor — and the body is human prose documenting the check. Manifests describe what the sensor is, not which stages use it; the relationship lives on the stage side via the stage's frontmatter sensors: field (see How stages import sensors below).

---
id: required-sections                       # required
kind: deterministic                          # required
command: aidlc engine sensor-required-sections   # required
default_severity: advisory                   # required
fire_on: gate                               # optional; write (default) | gate
description: Checks that stage output ...    # required
category: document-shape                     # optional
matches: "**/{aidlc-docs,intents}/**"                  # optional capability filter
input_schema:                                # optional
  output_path: string
  stage_slug: string
output_schema:                               # optional
  pass: boolean
  missing_headings: string[]
timeout_seconds: 5                           # optional
---

# required-sections sensor

<body — prose documenting default mode, override mode, failure mode>
Field Required Type Notes
id kebab-case string Equals filename stem minus aidlc- prefix; cross-referenced from rule files' pairing: field (see Rule System).
kind enum Only deterministic is accepted today; llm reserved for the v0.11.0 LLM-dispatch chapter. See kind enum below.
command string Canonical invocation prefix. Shipped sensors use a native delegate such as aidlc engine sensor-required-sections; third-party sensors may declare another runtime. The sensor dispatcher appends --stage <slug> plus --output-path <path> for document sensors or --file-path <path> for code sensors.
default_severity enum advisory or blocking. Blocking is enforced for fire_on: gate; write-fired blocking declarations remain advisory in this release.
description string One-line human description.
category optional string Free-form descriptive label (the shipped manifests use document-provenance, document-shape, and code-quality; not a closed enum).
fire_on optional enum write or gate; defaults to write.
matches optional glob string Capability filter consumed at dispatch. See matches filter below.
input_schema optional object Advisory today; future LLM dispatch will use it as a templating contract.
output_schema optional object Advisory today; future LLM dispatch will use it as a parsing contract.
timeout_seconds optional int Per-fire wall-clock cap.

kind enum

The kind field declares the dispatch mechanism. The schema accepts exactly one value today:

  • deterministic — the manifest's command: is a self-contained shell invocation that exits 0 (pass) / non-zero (fail) and writes structured detail to a known path.

llm is reserved for the LLM-dispatch chapter (v0.11.0+). Until that chapter ships, consumers MUST reject kind: llm at parse time. Reservation is enforced at write time: shipping a kind: llm manifest today is a manifest-author error that the parser rejects.

Unknown values for kind (anything other than deterministic) are rejected at parse time. Forward-compat applies to unknown keys (see Forward-compat policy) — not to unknown values for known keys.


How stages import sensors

Pull authoring: each stage's frontmatter declares the sensors it uses. The compile resolver looks each declared id up in the manifest registry and bakes a sensors_applicable array onto the stage's compiled graph node. Authoring direction is locality-of-reference — open a stage file and you see exactly which checks fire when the stage runs.

# dist/claude/.claude/aidlc-common/stages/construction/code-generation.md
---
slug: code-generation
phase: construction
# ...
requires_stage: [...]
sensors:
  - linter
  - type-check
inputs: ...
outputs: ...
---

sensors: is a list of bare ids — the ids match each manifest's frontmatter id: field, which (per the filename↔id contract) equals the filename stem minus the aidlc- prefix. The compile resolver:

  1. Walks dist/claude/.claude/sensors/, parses every aidlc-<id>.md manifest.
  2. Indexes manifests by id for O(1) lookup at resolution time.
  3. For each stage, looks each declared import id up; throws on unknown (loud failure at compile, not silent at fire time).
  4. Copies fire_on, default_severity, category, and matches into the resolved sensors_applicable[] entry.
  5. Emits the per-stage resolved array on the canonical data/stage-graph.json (FIELD_ORDER pinned: after rules_in_context).

The runtime PostToolUse hook, gate-start, and revise read sensors_applicable off the graph node — none re-opens the manifest. Dispatch fields are compile-snapshotted: a manifest edit during the workflow does NOT change what fires for the in-flight workflow (BGP-stability property — see Plane Architecture).

Per-stage sensor matrix (33 framework stages)

Stages sensors:
3 initialization (workspace-scaffold, workspace-detection, state-init) [] (deterministic setup, no agent-authored markdown)
intent-capture [claim-sources, required-sections, upstream-coverage] (claim-sources checks visible inline provenance, authoritative source-register values, and exact human-confirmed assumptions across the stage's deliverables)
6 other ideation, 6 other inception, 7 operation markdown stages [required-sections, upstream-coverage]
user-stories, domain-design, units-generation [required-sections, upstream-coverage, traceability]
build-and-test [required-sections, upstream-coverage, type-check] (linter intentionally omitted — build runs canonical lint)
ci-pipeline [required-sections, upstream-coverage, linter, type-check]
4 per-Unit construction-design stages (functional-design, infrastructure-design, nfr-design, nfr-requirements) [required-sections, upstream-coverage, linter, type-check, traceability]
code-generation [linter, type-check, traceability]

Forks customise stages by editing the stage's sensors: list directly — the binding lives next to the thing being customised. A manifest is a pure capability descriptor; it carries no stage-targeting field (there is no applies_to: — pull authoring removed it). The strict-additive runtime applies: if a fork wants a sensor on a stage, it imports it; if it does not, it omits it. There is no override layer to reason about.


matches filter

matches is an optional top-level capability descriptor on the manifest. It declares the glob shape of files the sensor can analyse — "this sensor analyses files matching this glob" — and is consumed at dispatch, not by the resolver at compile time.

Manifest matches
aidlc-claim-sources.md **/{aidlc-docs,intents}/**
aidlc-required-sections.md **/{aidlc-docs,intents}/**
aidlc-upstream-coverage.md **/{aidlc-docs,intents}/**
aidlc-traceability.md **/traceability.json
aidlc-linter.md **/*.{ts,js}
aidlc-type-check.md **/*.{ts,tsx}

For fire_on: write, matches is the fire filter: the hook compares the path being written against the glob and an entry without a glob never fires. For fire_on: gate, gate-start and revise enumerate every existing declared deliverable, skip paths outside each sensor's matches capability, and dispatch only matching paths; an omitted glob accepts every deliverable. All six shipped manifests declare a glob. The compile resolver copies it into sensors_applicable[].

Empty string (matches: "") is rejected at parse time. Write-fired sensors should declare a glob; gate-fired sensors may omit it to analyze every declared deliverable.

Cross-references between rules and sensors

Rule files use pairing: aidlc-required-sections (with the aidlc- prefix) to feed-forward into a sensor; the sensor manifest's id: is required-sections (no prefix). The doctor coverage check normalises by stripping the aidlc- prefix from the rule's pairing: value before matching against the manifest id.


default_severity

advisory outcomes produce their audit rows but do not block the stage gate. A blocking gate binding proceeds only on a verified pass. Reported findings, dispatcher exit/spawn/timeout failures, malformed or mismatched verdicts, SENSOR_BUDGET_OVERRIDE, and SENSOR_PASSED rows carrying tool-unavailable or script-error all stop gate-start, revise, or approve-time recovered revision re-entry before the gate opens.

The operator can fix the findings and retry, or make a separate explicit override decision. The conductor records a DECISION_RECORDED offering Fix findings,Override blocking sensors, waits for a new human turn, records the exact QUESTION_ANSWERED, then retries the gate report with --override-blocking-sensors --user-input "Override blocking sensors". A bare flag, an unoffered/paraphrased choice, a missing human-backed receipt, or autonomous mode is refused. A successful override records the sensor ids, optional detail paths, and evaluation reasons on STAGE_AWAITING_APPROVAL. Revalidating an already-open gate emits a fresh row with Revalidated: true, consuming the authorization receipt instead of leaving it reusable. In this release, a write-fired sensor may declare blocking, but PostToolUse dispatch remains advisory.


fire_on

write is the default and preserves incremental PostToolUse feedback. gate fires once per existing declared deliverable immediately before gate-start opens the first gate, before revise re-enters the gate after revision work, and before the approve-time revision backstop performs recovered re-entry. Dispatch happens outside the state transaction because aidlc-sensor.ts fire takes the audit lock around both its SENSOR_FIRED and terminal rows. Blocking dispatch fingerprints every matching artifact before evaluation, checks that fingerprint after each sensor, and checks it again inside the state transaction. Changed bytes refuse gate entry and must be evaluated on a retry.

The dispatcher prints one compact JSON verdict after the terminal row: fire_id, sensor_id, stage, output_path, result, detail_path, and an optional note. Gate enforcement validates the verdict identity and treats anything other than an unnoted passed result as non-passing for a blocking binding. Explicit --artifacts paths and discovered deliverables are resolved canonically and must remain inside the stage's canonical produce directories; absolute paths, traversal, and symlink escapes cannot redirect a sensor.


command: invocation contract

The manifest's command: is the canonical invocation prefix, not the full argv — each shipped sensor names its own per-sensor script. The dispatcher (aidlc-sensor.ts) appends runtime context at fire time: always --stage <stage-slug>, then the file flag matching the sensor's input shape — --output-path <file> for document sensors, --file-path <file> for the code sensors (linter, type-check):

<command> --stage <stage-slug> --output-path <file-being-written>   # document sensor
<command> --stage <stage-slug> --file-path   <file-being-written>   # code sensor

So a manifest with:

command: aidlc engine sensor-required-sections

invoked against requirements-analysis writing the requirements artifact in the intent's record dir is dispatched as:

aidlc engine sensor-required-sections \
  --stage requirements-analysis \
  --output-path aidlc/spaces/default/intents/260624-inventory-api/inception/requirements-analysis/requirements.md

The manifest does not encode the per-fire flags. The dispatcher appends them; the manifest stays a pure capability descriptor.


Gate-ritual handoff (surface stdout / selections-file in)

The §13 learning gate is tool-as-actor. The round-trip between the deterministic tool (aidlc-learnings.ts) and the conductor (the live /aidlc session) has two legs, with a knowledge step and a judgement step between them:

  1. surface (stdout). aidlc engine learnings surface --slug <stage-slug> reads the stage's memory.md and prints structured JSON: candidates[] (one per non-blank Interpretation / Deviation / Tradeoff entry, each carrying id, source_heading, ts, summary, context, default_scope: "project") plus a read-only parked_open_questions[]. No AskUserQuestion field names — pure domain data. Open questions never become candidates (they are research items).
  2. Conductor renders the AskUserQuestion (knowledge). One option per candidate (label = the candidate summary, verbatim; description = the derived destination, e.g. → memory/project.md (Deviation) plus a promote-to-team affordance). After multiSelect, the conductor correlates each kept label back to its candidate id + source_heading. It then always asks "Anything to add for next time?"; any free-text gets a single heading-pick AUQ (Interpretation / Deviation / Tradeoff / Open question) — the heading pick is the user's only classification, and the destination is derived from it.
  3. Admission conflict-check (knowledge → orchestrator-LLM; gates which selections reach persist). For each kept learning, the conductor compares the single proposed dated entry against org.md's matching ## <section> (the single-line variant of the §5 admission gate). On a contradiction the conductor surfaces the conflicting org sentence inline and the user revises / skips / escalates (judgement → user; no user-override path). Only conflict-clear or user-escalated selections proceed. Sensor manifests have no org-section analogue and skip the check.
  4. persist (selections-file in). The conductor writes the kept selections to <record>/.aidlc-learnings/<slug>-selections.json (in the intent's record dir) (gitignored) and calls aidlc engine learnings persist --slug <slug> --selections-json <path>. The tool is the deterministic writer — it never judges conflicts; it routes each learning as a practice to aidlc/spaces/<surface-time-space>/memory/{project,team}.md and, for a sensor selection, does the two-write install (manifest + originating stage sensors: frontmatter) inside one withAuditLock, then emits RULE_LEARNED / SENSOR_PROPOSED.

The selections-file is the replay artefact: a crashed persist replays the same JSON without re-prompting the human (content-presence idempotency via a <!-- cid:<intent-slug>:<slug>:<content-hash> --> marker per written line — the full SHA-256 hash of the learning's own text, not its positional candidate id). The selections-file also carries space/intent, bound once when the candidates were surfaced; persist uses those, never re-resolving the live active-intent cursor itself. Before writing, it verifies that this space and any non-null intent record still exist and that the requested slug matches the surface-time stage_slug.


Defaults for scaffolded manifests

When a sensor proposal is confirmed at the gate, the gate-ritual tool scaffolds a new project-tier manifest at <project>/.claude/sensors/aidlc-<id>.md — never the shipped framework distribution (a per-project learning loop must not mutate the framework; framework-distribution paths are rejected). Fields default to:

Field Default Note
id derived from user free-text (kebab-case it)
kind deterministic sole accepted value today
command bun ./plugins/acme/aidlc-sensor-<id>.ts third-party Bun-backed example; the plugin must declare that runtime requirement
default_severity advisory non-blocking default
fire_on write incremental write dispatch
description from user free-text
category "" user fills if desired
matches write-path glob scaffold prompts for the glob shape the sensor applies to (an artifact-tree glob or a code glob like **/*.ts); a write-fired entry with no matches never fires
input_schema { output_path: string, stage_slug: string } matches the dispatcher-appended flags
output_schema { pass: boolean } minimum structure dispatcher relies on
timeout_seconds 30 conservative default; tune for slower dispatchers

After scaffolding the manifest, the gate-ritual tool — inside the same withAuditLock transaction — appends the new id to the originating stage's sensors: frontmatter list (the pull-authoring two-write install). The sensor is fully wired when the next workflow compiles. This is the one sanctioned stage-frontmatter edit: it grows the import list (immutable in shape, not in contents), never the ## Steps / ## Sensors / ## Learn body.

The six shipped manifests illustrate the variation these defaults later evolve into: aidlc-claim-sources.md, aidlc-required-sections.md, and aidlc-upstream-coverage.md use timeout_seconds: 5 with their artifact-tree matches glob (the value shown in the matches table above); aidlc-linter.md uses 30 with matches: "**/*.{ts,js}"; aidlc-type-check.md uses 60 with matches: "**/*.{ts,tsx}".


Forward-compat policy

Consumers of sensor manifests (compile, dispatcher, gate-ritual scaffolding, doctor) MUST tolerate unknown manifest keys. If a future release adds an optional cool_new_field:, older consumers parse the manifest, ignore the field, and continue. This allows additive evolution of the schema without breaking forks or pre-upgrade workspaces.

Forward-compat does NOT apply to unknown values for known keys. As documented in kind enum above, an unknown value for kind is rejected at parse time. The same principle applies to the other enum-shaped fields (default_severity, fire_on).


Reserved for future releases

A few sensor capabilities are reserved in the schema but not yet active, so the field shape is stable when they land:

  • kind: llm dispatch — LLM-evaluated sensors (v0.11.0). The schema accepts kind today but rejects any value other than deterministic at parse time.
  • Write-time blockingblocking is accepted on write-fired manifests, but only gate-fired failures are enforced in this release.

Both are enforced at write time: shipping a manifest that uses them now is an author error that the parser rejects.

Next Steps

  • Rules — the feedforward half of the control loop pairs with these sensors via the pairing: field. See Rule System.
  • The user-facing learning loop — how sensor proposals are surfaced and confirmed at the gate, and how a confirmed proposal scaffolds a new manifest. See Rules and the Learning Loop in the User Guide.
  • The compile boundary — how sensors_applicable is resolved once at workflow start and read off the graph node at fire time. See Plane Architecture.

The schema above plus the five shipped manifests in dist/claude/.claude/sensors/ are the working examples.