---
type: schema
applies_to_milestone: v1.5.5
created: 2026-05-06
status: active
consumers:
  - bin/lib/critic-dispatch.js
  - agents/gmd-critic.md
  - .planning/specs/gmd-critic-protocol.md
  - workflows/* (Phase 51)
  - tests/audit-critic-dispatcher-coverage.sh (Phase 51)
  - bin/lib/vault-critic-telemetry.js (CRIT-TELM-02; post-v1.5.5 free-floating)
---

# Critic Observations Schema (NDJSON v1) + Reciprocal-Marker Convention

Authoritative reference for the critic-observation NDJSON schema written to
`.planning/specs/gmd-critic-data/observations.ndjson` and for the reciprocal
`<!-- critic-decision: <uuid> -->` HTML comment marker emitted in consuming
artifacts (PLAN.md tasks, VERIFICATION.md findings, MILESTONE-AUDIT.md
entries) when a critic-influenced path is chosen.

Citation surface for `bin/lib/critic-dispatch.js` JSDoc (Plan 50-01),
`agents/gmd-critic.md` (Plan 50-02), Phase 51 workflow integration
(CRIT-WIRE-01..05), and the future `bin/lib/vault-critic-telemetry.js`
correlation routine (CRIT-TELM-02; FOUND-05-style free-floating post-v1.5.5).

The JSON output contract (`verdict` / `alternative_argument` /
`missed_concerns` / `escalate_to_human` / `confidence`) is sourced from
`.planning/specs/gmd-critic-protocol.md` §2 — that file is canonical; this
file documents the **persisted observation envelope** wrapping each response.

## NDJSON schema v1 — full field reference

19 fields: 15 from the v1.4 critic-protocol §3 schema (`schema_version: 0`
implicit for legacy entries 1–17) + 4 new fields added per CRIT-SCHEMA-01.

| #  | Field                 | Type                          | Redacted | Description                                                                                              |
|----|-----------------------|-------------------------------|----------|----------------------------------------------------------------------------------------------------------|
| 1  | `timestamp`           | ISO-8601 UTC string           | no       | Invocation time                                                                                           |
| 2  | `workflow`            | string                        | no       | `/gmd:new-milestone`, `/gmd:plan-phase`, `/gmd:discuss-phase`, ...                                        |
| 3  | `phase`               | string                        | no       | Short phase descriptor (`scope-decision`, `requirements-authoring`, `plan-task-breakdown`, ...)           |
| 4  | `decision_point`      | string                        | **yes**  | One-line description of the question (D8 redaction)                                                       |
| 5  | `lead_recommendation` | string                        | **yes**  | The menu option lead recommended including rationale (D8 redaction)                                       |
| 6  | `lead_rationale`      | string                        | no       | One-line summary of why lead recommended it                                                               |
| 7  | `critic_response`     | `endorse \| dissent \| refine`| no       | Critic verdict bucket                                                                                     |
| 8  | `critic_alternative`  | string \| null                | **yes**  | Critic's alternative (D8 redaction); `null` when verdict is `endorse`                                     |
| 9  | `outcome`             | string                        | no       | `locked_unchanged \| changed \| human_overrode \| escalated \| pending`                                   |
| 10 | `signal_class`        | string                        | no       | `useful \| redundant \| harmful \| unclassified`                                                          |
| 11 | `latency_seconds`     | int                           | no       | Wall-clock seconds for the critic round                                                                   |
| 12 | `tokens_input`        | int                           | no       | Estimate of input-token count                                                                             |
| 13 | `tokens_output`       | int                           | no       | Estimate of output-token count                                                                            |
| 14 | `cross_model_used`    | bool                          | no       | `true` only when D7 cross-model opt-in was used                                                           |
| 15 | `notes`               | string \| null                | **yes**  | Freeform context that doesn't fit structured fields (D8 redaction)                                        |
| 16 | `schema_version`      | int                           | no       | **NEW v1.** Value `1` for v1.5.5+ entries. Absent on legacy entries (treat as `0` implicit).              |
| 17 | `decision_id`         | UUID v4 string                | no       | **NEW v1.** Generated at INVOCATION TIME by the dispatcher per CRIT-DISP-02.                              |
| 18 | `affects_paths`       | string[] (may be empty)       | no       | **NEW v1.** File paths the decision affects (e.g., `[".planning/milestones/v1.5.5-SPEC.md"]`).            |
| 19 | `affects_reqs`        | string[] (may be empty)       | no       | **NEW v1.** REQ IDs the decision affects (e.g., `["CRIT-DISP-02"]`).                                      |

### v0 vs v1 entry handling

- **Legacy entries 1–17** in `observations.ndjson` do NOT carry the
  `schema_version` field. Readers MUST treat absence as `schema_version: 0`
  implicit. Existing fields preserved verbatim — never rewrite legacy entries
  to back-fill v1 fields.
- **Entries from v1.5.5 onward** write `schema_version: 1` explicitly in
  every record.
- **Entry #18** (`decision_id` `9104e545-a879-4b4e-a036-5b3a476e1c9d`,
  logged 2026-05-06 at v1.5.5 SPEC lock) is the FIRST `schema_version: 1`
  entry. It is preserved verbatim as the on-disk reference instance — the
  dispatcher's round-trip integrity test (Plan 50-01) reads it and asserts
  byte-identity after parse + serialize + redact.

### Worked example (v1 entry, sanitized shape)

```json
{
  "timestamp": "2026-05-06T18:00:00Z",
  "schema_version": 1,
  "decision_id": "9104e545-a879-4b4e-a036-5b3a476e1c9d",
  "workflow": "/gmd:new-milestone",
  "phase": "v1.5.5-spec-lock",
  "decision_point": "Lock v1.5.5 gmd-critic milestone SPEC ...",
  "lead_recommendation": "Lock SPEC as drafted: HF-shape (Gate 3 cites <redacted> precedent) ...",
  "lead_rationale": "Approach C+ resolves L1; HF-shape avoids closure-substrate ceremony",
  "critic_response": "refine",
  "critic_alternative": "R1: Upgrade Gate 3 to FULL-SUBSTRATE — <redacted> ...",
  "outcome": "changed",
  "signal_class": "useful",
  "latency_seconds": 137,
  "tokens_input": 18000,
  "tokens_output": 3500,
  "cross_model_used": false,
  "affects_paths": [".planning/milestones/v1.5.5-SPEC.md", ".planning/specs/gmd-critic-data/observations.ndjson"],
  "affects_reqs": [],
  "notes": "Eat-your-own-dogfood proof per L8 / D11 / Gate 7. First entry under <redacted>."
}
```

`<redacted>` reflects D8 high-entropy regex output. Entry #18 on disk
preserves the originals (logged before the dispatcher's D8 redactor
shipped); the redactor applies to entries the dispatcher writes going
forward, not to legacy or pre-dispatcher hand-logged entries.

## PII redaction policy (D8)

Source-of-record: TELM-06's high-entropy redactor in
`bin/lib/vault-telemetry.js:69`. The regex is:

```
/[A-Za-z0-9_-]{20,}/g
```

Replacement: literal `<redacted>`.

Applied to **4 named fields only**:

- `decision_point`
- `lead_recommendation`
- `critic_alternative`
- `notes`

Application timing: **before NDJSON serialization**, inside
`bin/lib/critic-dispatch.js` (Plan 50-01). The 15 non-redacted fields
(timestamp, workflow, phase, lead_rationale, critic_response, outcome,
signal_class, latency_seconds, tokens_input, tokens_output, cross_model_used,
schema_version, decision_id, affects_paths, affects_reqs) are written as-is.

**No new redaction infrastructure** — D8 mandates TELM-06 reuse. Per Plan
50-01 planner judgment, the dispatcher re-declares the regex locally rather
than `require('./vault-telemetry.js')` (whose require-chain assumes a
vault-root and would couple the harness-neutral dispatcher to the obsidian
subsystem). The regex string is the contract; source-of-record citation is
enforced by the Phase 51 audit.

### Worked example

Input → output:

```
key_AbCdEfGhIjKlMnOpQrStUvWxYz123_end   →   key_<redacted>_end
```

The 26-char run is matched and replaced; the threshold is 20 consecutive
chars from `[A-Za-z0-9_-]`. Short identifiers, REQ IDs (`CRIT-DISP-02`,
11 chars), and ISO timestamps (in fields outside the 4-field redaction set)
are unaffected.

## CRIT-TELM-01a reciprocal-marker convention (CRIT-SCHEMA-03)

When a workflow invokes the dispatcher AND the lead adopts the critic-
influenced path, the consuming artifact (PLAN.md task / VERIFICATION.md
finding / MILESTONE-AUDIT.md entry) emits a reciprocal HTML comment marker
naming the corresponding NDJSON entry's `decision_id`:

```
<!-- critic-decision: 9104e545-a879-4b4e-a036-5b3a476e1c9d -->
```

The UUID is the same UUID v4 written to the NDJSON entry's `decision_id`
field. This is why CRIT-DISP-02 mandates **invocation-time** UUID generation
(not result-write time): the workflow needs the UUID in scope BEFORE it
writes the consuming artifact, so it can include the marker AND surface the
UUID in user-visible menu copy ("critic decision X applied — see ...") in
the same flow.

**Workflow guidance (advisory; non-normative for v1.5.5):** place the marker
immediately above the affected line or section in the consuming artifact.
Example placement above the v1.5.5 SPEC §3 scope-item-#8 line in
`.planning/milestones/v1.5.5-SPEC.md`:

```markdown
<!-- critic-decision: 9104e545-a879-4b4e-a036-5b3a476e1c9d -->
8. VERIFY-CRITIC-01 closure substrate (full-substrate; CRIT-CLOSURE-01..04).
```

### v1.5.5 = convention only

No audit enforcement of marker emission ships in v1.5.5. Workflow authors
emit the marker by discipline; absent markers in v1.5.5-era artifacts will
NOT fail any audit during the v1.5.5 milestone window.

Phase 51 CRIT-WIRE-05 verifies that workflow source-files INVOKE marker
emission (i.e., the workflow body contains the literal write of
`<!-- critic-decision: ... -->`) but does NOT verify post-execution that the
artifact actually carries the marker. Post-execution artifact-side audit
enforcement is deferred to **CRIT-TELM-02** (FOUND-05-style free-floating
post-v1.5.5; lands when the lead-machine corpus reaches N entries with
`schema_version: 1`).

### Why ship the marker NOW (per critic R4 — not deferrable)

Without the marker shipping at schema-design time, CRIT-TELM-02's
outcome-class join (`validated` / `false_positive` / `missed` / `confirmed`)
is **foreclosed for v1.5.5-era entries**. The join reads each NDJSON entry's
`decision_id`, walks outward to find the marker in the consuming artifact,
then reads surrounding context (PASS/FAIL, human override, regression catch)
to classify the outcome. Without markers, CRIT-TELM-02 has two paths:

1. **Retroactive artifact annotation** — open every PLAN.md /
   VERIFICATION.md / MILESTONE-AUDIT.md authored during v1.5.5 and inject
   markers post-hoc. Real edit work, breaks chronology-anchor
   pre-registration discipline (TD-07 archetype), depends on memory of
   which decisions affected which artifacts.

2. **Loss of join semantics** — v1.5.5-era entries (#18..N) become
   permanently unjoinable; CRIT-TELM-02 ships v1.5.5-data-blind.

Both paths violate Locked Principle P3 (instrumentation first, analytics
later). Schema + marker convention BOTH ship at schema-design time;
correlation routine ships later — that is the discipline.

## Lifecycle & evolution

- **Additive evolution.** New fields land via `schema_version` bump. v0→v1
  added 4 fields without renaming, removing, or re-typing any of the
  original 15. A hypothetical v2 (when CRIT-TELM-02 ships post-v1.5.5)
  would add `outcome_class` (`validated` / `false_positive` / `missed` /
  `confirmed`) under the same discipline.
- **Append-only.** NEVER mutate prior entries. Serialization-shape fixes
  land as `schema_version: N+1` on new entries; old entries stay as-is.
- **Atomic-append semantics.** Dispatcher uses `fs.appendFileSync`.
  Sub-`PIPE_BUF` (4096-byte) writes with `O_APPEND` are atomic across
  concurrent appenders on POSIX and Windows. NDJSON entries (~1.5KB
  typical; entry #18 ~3KB) stay under threshold; concurrent invocations
  interleave at entry boundaries, never mid-entry.

## Citations

- `.planning/specs/gmd-critic-protocol.md` §2 — JSON output contract
  (`verdict` / `alternative_argument` / `missed_concerns` /
  `escalate_to_human` / `confidence`); canonical source-of-record.
- `.planning/specs/gmd-critic-protocol.md` §3 — original 15-field NDJSON
  schema (v0 baseline this schema extends).
- `bin/lib/critic-dispatch.js` — dispatcher SoT (Plan 50-01); generates
  `decision_id`, applies D8 redaction, atomic-appends.
- `agents/gmd-critic.md` — JSON-envelope producer (Plan 50-02).
- `bin/lib/vault-telemetry.js:69` — TELM-06 `[A-Za-z0-9_-]{20,}` regex
  source-of-record reused per D8.
- `.planning/milestones/v1.5.5-SPEC.md` — D2, D7, D8, D12; scope items
  #3 (reciprocal marker) and #8 (closure substrate).
- `.planning/REQUIREMENTS.md` — CRIT-SCHEMA-01..03, CRIT-DISP-02,
  CRIT-AGENT-02, CRIT-DOGFOOD-01.
- `.planning/specs/gmd-critic-data/observations.ndjson` — append-only data
  file; entry #18 is the on-disk v1 reference instance.
