harness-everything 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.agents/harness-everything/project-docs.json +3 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +17 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
  6. package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
  7. package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
  8. package/ci/mechanism-325-f4-projections.test.js +11 -1
  9. package/ci/mechanism-prb-outline-review.test.js +1265 -0
  10. package/docs/adr/DOCS-001-project-docs-config.md +32 -0
  11. package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
  12. package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
  13. package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
  14. package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
  15. package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
  16. package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
  17. package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
  18. package/docs/doc-workflow-reconciliation.md +30 -0
  19. package/docs/governed-index.md +8 -0
  20. package/docs/platform-capabilities.md +2 -0
  21. package/harness-everything/SKILL.md +1 -1
  22. package/harness-everything/scripts/doc-evidence.js +55 -0
  23. package/hooks/hooks.json +1 -1
  24. package/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
  25. package/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
  26. package/hooks/scripts/lib/doc-reconciliation-provider.js +861 -6
  27. package/hooks/scripts/state-persist.js +10 -0
  28. package/opencode-plugin/plugin.json +1 -1
  29. package/package.json +1 -1
  30. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  31. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
  32. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
  33. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +861 -6
  34. package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
  35. package/plugins/harness-everything/plugin.json +1 -1
  36. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  37. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
  38. package/sbom.cdx.json +7 -7
@@ -0,0 +1,32 @@
1
+ ---
2
+ id: DOCS-001
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-001 — Project docs config resolution (D5)
7
+
8
+ ## Decision
9
+
10
+ - The committed, host-neutral `.agents/harness-everything/project-docs.json` wholly overrides differing host `projectDocs` values.
11
+ - `AGENTS.md` is never a configuration source.
12
+ - When the project file overrides a differing host value, the resolver reports `project-overrides-host`.
13
+
14
+ ## Context
15
+
16
+ | Issue | Subject | State |
17
+ | --- | --- | --- |
18
+ | #325 | Documentation family, PRD/ADR governance and doc-change workflow | OPEN |
19
+
20
+ - Contract: [document-lifecycle.md § Configuration (D5)](../document-lifecycle.md#configuration-d5).
21
+
22
+ ## Evidence
23
+
24
+ - Implementation: [project-docs-resolver.js](../../harness-everything/scripts/project-docs-resolver.js) (`project-overrides-host`, around line 390), [doc-check.js](../../harness-everything/scripts/doc-check.js).
25
+ - Test: [mechanism-2aw-project-docs-config.test.js](../../ci/mechanism-2aw-project-docs-config.test.js).
26
+ - Repo-wide gate, not row-specific: [reference-check.js](../../ci/reference-check.js).
27
+ - `AGENTS.md` exclusion is stated in [document-lifecycle.md](../document-lifecycle.md) (configuration table). The resolver has no `AGENTS.md` read path.
28
+ - Config in this repo: [project-docs.json](../../.agents/harness-everything/project-docs.json).
29
+
30
+ ## Open gates
31
+
32
+ - #325 is OPEN as the documentation-family umbrella. Shipped slices do not close it.
@@ -0,0 +1,34 @@
1
+ ---
2
+ id: DOCS-002
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-002 — ADR/REQ identity, supersession and generated index (D6.1–D6.4)
7
+
8
+ ## Decision
9
+
10
+ - ADR IDs are `<DOMAIN>-NNN` (domain 2–10 uppercase alphanumerics, at least 3 digits). The domain must be declared in `domains`.
11
+ - `status` is required and uses `proposed` / `accepted` / `rejected` / `superseded`.
12
+ - Aliases and `legacy:` references resolve to one document. Self-aliases and collisions are diagnosed.
13
+ - A `supersedes` edge takes effect only from an `accepted` ADR to a target already `superseded`. Cycles are diagnosed.
14
+ - `conflicts_with` stays a recorded conflict until an effective `supersedes` edge exists.
15
+ - The index in `docs/governed-index.md` is generated, not hand-written.
16
+
17
+ ## Context
18
+
19
+ | Issue | Subject | State |
20
+ | --- | --- | --- |
21
+ | #325 | Documentation family, PRD/ADR governance and doc-change workflow | OPEN |
22
+
23
+ - Contract: [document-lifecycle.md § IDs](../document-lifecycle.md#ids-aliases-and-references-d62-d68), [§ Supersession](../document-lifecycle.md#supersession-d63-d64).
24
+
25
+ ## Evidence
26
+
27
+ - Implementation: [doc-lifecycle.js](../../harness-everything/scripts/doc-lifecycle.js) (`ADR_ID` at line 20; `unknown-domain` at line 1078; `supersede-cycle` at line 1220).
28
+ - Test: [mechanism-2ax-doc-lifecycle.test.js](../../ci/mechanism-2ax-doc-lifecycle.test.js).
29
+ - Repo-wide gate, not row-specific: [reference-check.js](../../ci/reference-check.js).
30
+ - Generator: [governed-index.md](../governed-index.md), produced by `doc-lifecycle.js index` (see `derived` in [project-docs.json](../../.agents/harness-everything/project-docs.json)).
31
+
32
+ ## Open gates
33
+
34
+ - #325 is OPEN as the documentation-family umbrella. Shipped slices do not close it.
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: DOCS-003
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-003 — Contract-style PRD revision chain (D6.9)
7
+
8
+ ## Decision
9
+
10
+ - An effective PRD is a frozen revision chain with an approval record.
11
+ - A change creates a new complete revision copied from the current effective revision. The effective snapshot is never edited in place.
12
+ - This replaces the D6.1 living-PRD model for PRDs only. ADRs, tickets and other roles are unchanged.
13
+
14
+ ## Context
15
+
16
+ | Issue | Subject | State |
17
+ | --- | --- | --- |
18
+ | #325 | Documentation family, PRD/ADR governance and doc-change workflow | OPEN |
19
+
20
+ - Contract: [document-lifecycle.md § Contract-style PRD revisions (D6.9)](../document-lifecycle.md#contract-style-prd-revisions-d69).
21
+
22
+ ## Evidence
23
+
24
+ - Implementation: [doc-lifecycle.js](../../harness-everything/scripts/doc-lifecycle.js) (revision identity, approval, immutability and `immutable-revision-modified` diagnostics).
25
+ - Test: [mechanism-2ay-prd-revisions.test.js](../../ci/mechanism-2ay-prd-revisions.test.js). Its header cites the D6.9 contract section.
26
+
27
+ ## Open gates
28
+
29
+ - #325 is OPEN as the documentation-family umbrella. Shipped slices do not close it.
@@ -0,0 +1,28 @@
1
+ ---
2
+ id: DOCS-004
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-004 — `doc-change` step type
7
+
8
+ ## Decision
9
+
10
+ - `doc-change` is an explicit documentation-edit step type.
11
+ - It has no implicit TDD binding. Step types are declared in a registry, not inferred.
12
+
13
+ ## Context
14
+
15
+ | Issue | Subject | State |
16
+ | --- | --- | --- |
17
+ | #326 | Extract step-type registry and add doc-change step (split from #320 for #325) | CLOSED |
18
+ | #328 | Declarative step-type registry with declared-only doc-change (#326) | MERGED |
19
+
20
+ ## Evidence
21
+
22
+ - Implementation: [step-types.json](../../hooks/scripts/lib/step-types.json) (`"id": "doc-change"`), [workflow-obligations.js](../../hooks/scripts/lib/workflow-obligations.js).
23
+ - Test: [mechanism-2az-step-type-registry.test.js](../../ci/mechanism-2az-step-type-registry.test.js).
24
+
25
+ ## Open gates
26
+
27
+ - None for #326 and #328.
28
+ - #325 is OPEN as the documentation-family umbrella (see DOCS-001).
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: DOCS-005
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-005 — Doc obligations and trusted evidence producer (phase 3a)
7
+
8
+ ## Decision
9
+
10
+ - Documentation MUST obligations fail closed: missing or untrusted evidence yields a failing result, never a pass.
11
+ - Evidence is produced by a trusted producer. Baseline-bound evidence is refused when the baseline has no whole-baseline digest.
12
+
13
+ ## Context
14
+
15
+ | Issue | Subject | State |
16
+ | --- | --- | --- |
17
+ | #334 | Phase 3a doc-change obligation derivation and evidence producer (#325) | MERGED |
18
+ | #335 | Bind deleted and renamed paths to baseline evidence | CLOSED |
19
+ | #342 | Read baseline and destination identities exactly once (fix for #335) | MERGED |
20
+
21
+ ## Evidence
22
+
23
+ - Implementation: [doc-obligations.js](../../harness-everything/scripts/doc-obligations.js), [doc-evidence.js](../../harness-everything/scripts/doc-evidence.js) (digest refusal near line 165), [doc-evidence-capture.js](../../harness-everything/scripts/doc-evidence-capture.js).
24
+ - Tests: [mechanism-325-3a-doc-obligations.test.js](../../ci/mechanism-325-3a-doc-obligations.test.js), [mechanism-325-3a-doc-evidence.test.js](../../ci/mechanism-325-3a-doc-evidence.test.js), [mechanism-335-baseline-evidence.test.js](../../ci/mechanism-335-baseline-evidence.test.js).
25
+
26
+ ## Open gates
27
+
28
+ - None specific to #334, #335 or #342.
29
+ - #325 is OPEN as the documentation-family umbrella (see DOCS-001).
@@ -0,0 +1,50 @@
1
+ ---
2
+ id: DOCS-006
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-006 — Doc reconciliation pilot (S1 default-OFF, F4 limited credit)
7
+
8
+ ## Decision
9
+
10
+ - The S1 runtime is default OFF. It runs only when `HARNESS_DOC_RECONCILIATION_PILOT=1`.
11
+ - Fail-closed: evidence that is not trusted is withheld, not credited.
12
+ - F4 gives positive credit for `doc-lint` and `link-check`, from a host-captured run of the installed doc-check. The contract limits positive credit to Claude Code ([F4 contract](../doc-workflow-reconciliation.md#f4-credited-reconciliation-claude-only-pilot-on), line 1158).
13
+ - F4 also gives credit for `quality-review` from an independent host reviewer (`SubagentStop`).
14
+ - `conflict-check` (PR #385): `doc-check.js check --conflict-assessment` requires an exact `--base` SHA and runs lifecycle lint with `--all`. It credits only a complete corpus with no lint error diagnostics, and checks recorded and structural conflicts only. Prose contradictions stay with `quality-review`. Plain `check` earns no conflict credit.
15
+ - The observed live runs (Claude Code 2.1.309, runs 4–5) credited only `doc-lint` and `link-check`. `quality-review` and `conflict-check` have mechanism tests only.
16
+ - Other capabilities stay withheld. Expansion beyond the limited pilot is gated on #341 E1.
17
+
18
+ ```mermaid
19
+ flowchart LR
20
+ E["HARNESS_DOC_RECONCILIATION_PILOT=1"] --> P["doc-reconciliation-pilot.js"]
21
+ P -->|"host-captured doc-check"| L["doc-lint + link-check: credited"]
22
+ P -->|"independent SubagentStop reviewer"| Q["quality-review: credited (mechanism tests)"]
23
+ P -->|"check --conflict-assessment, complete corpus"| C["conflict-check: credited (mechanism tests)"]
24
+ P -. "all other capabilities" .-> W["withheld"]
25
+ ```
26
+
27
+ ## Context
28
+
29
+ | Issue | Subject | State |
30
+ | --- | --- | --- |
31
+ | #341 | Phased snapshot boundary, conformance suite, obligation contract | OPEN |
32
+ | #350 | Docuvia pilot limitations (code-to-doc dependency edge, derivation hygiene) | OPEN |
33
+ | #358 | Credit host-captured doc evidence on Claude Code (#325) | MERGED |
34
+ | #375 | Retain adjudicated E1 blockers and Phase 3c handoff | MERGED |
35
+ | #385 | Conflict-check producer from a full-corpus assessment | MERGED |
36
+
37
+ - #375 records zero qualifying E1 cycles, so expansion stays gated.
38
+ - Contract: [doc-workflow-reconciliation.md § S1 switch](../doc-workflow-reconciliation.md#s1--switch-trusted-scope-and-fail-closed-no-credit-gate).
39
+
40
+ ## Evidence
41
+
42
+ - Implementation: [doc-reconciliation-pilot.js](../../hooks/scripts/lib/doc-reconciliation-pilot.js) (env gate at line 8; `doc-lint` / `link-check` credit at line 246; `quality-review` collection from `SubagentStop` at lines 244 and 254; `conflict-check` collection at lines 251–252), [doc-check.js](../../harness-everything/scripts/doc-check.js) (flag parse at lines 86–88; `--base` required at line 102; `--all` lint at line 108; assessment at lines 468–498; credit at line 501), [doc-reconciliation-provider.js](../../hooks/scripts/lib/doc-reconciliation-provider.js) (corpus binding check at lines 763–785), [doc-reconciliation-core.js](../../hooks/scripts/lib/doc-reconciliation-core.js), [doc-reconciliation-producer.js](../../hooks/scripts/lib/doc-reconciliation-producer.js), [doc-reconciliation-events.js](../../hooks/scripts/lib/doc-reconciliation-events.js).
43
+ - F4 contract: [doc-workflow-reconciliation.md § F4](../doc-workflow-reconciliation.md#f4-credited-reconciliation-claude-only-pilot-on). `conflict-check` row: [doc-workflow-reconciliation.md](../doc-workflow-reconciliation.md) line 1174.
44
+ - Tests: [mechanism-pr-a-conflict-check.test.js](../../ci/mechanism-pr-a-conflict-check.test.js) (17/17 PASS at this SHA; mechanism only, no live-host run), [mechanism-325-3b-preparation.test.js](../../ci/mechanism-325-3b-preparation.test.js), [mechanism-325-3b-s1-review.test.js](../../ci/mechanism-325-3b-s1-review.test.js), [mechanism-325-3b-s2a-core.test.js](../../ci/mechanism-325-3b-s2a-core.test.js), [mechanism-325-3b-s2b-wiring.test.js](../../ci/mechanism-325-3b-s2b-wiring.test.js), [mechanism-341-opencode-lane.test.js](../../ci/mechanism-341-opencode-lane.test.js).
45
+
46
+ ## Open gates
47
+
48
+ - #341 OPEN: E1 GO / REVISE / NO-GO gates any expansion beyond the limited pilot.
49
+ - #350 OPEN: Docuvia pilot limitations.
50
+ - #325 OPEN as the documentation-family umbrella (see DOCS-001).
@@ -0,0 +1,31 @@
1
+ ---
2
+ id: DOCS-007
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-007 — Shared doc craft and derived-view regeneration (F2/F3)
7
+
8
+ ## Decision
9
+
10
+ - Each declared derived view in `derived` MUST be regenerated and must match its source when its inputs change (`derived-regenerate`).
11
+ - Doc-skill craft guidance is shared from one place under `references/documentation/_shared/`, not copied into each skill.
12
+
13
+ ## Context
14
+
15
+ | Issue | Subject | State |
16
+ | --- | --- | --- |
17
+ | #356 | Share documentation craft across doc skills (#325) | MERGED |
18
+ | #357 | Check derived views with regenerate-equals (#325) | MERGED |
19
+
20
+ - #325 is OPEN as the documentation-family umbrella (see DOCS-001). It is the commit-title parent of both merged PRs.
21
+
22
+ ## Evidence
23
+
24
+ - Implementation: [doc-obligations.js](../../harness-everything/scripts/doc-obligations.js) (`derived-regenerate` capability at line 10), [writing-craft.md](../../harness-everything/references/documentation/_shared/writing-craft.md), [maintenance.md](../../harness-everything/references/documentation/_shared/maintenance.md).
25
+ - Tests: [mechanism-325-f3-derived-views.test.js](../../ci/mechanism-325-f3-derived-views.test.js), [mechanism-325-f2-doc-craft.test.js](../../ci/mechanism-325-f2-doc-craft.test.js).
26
+ - Repo-wide gate, not row-specific: [reference-check.js](../../ci/reference-check.js).
27
+
28
+ ## Open gates
29
+
30
+ - None specific to #356 or #357.
31
+ - #325 is OPEN as the documentation-family umbrella (see DOCS-001).
@@ -0,0 +1,30 @@
1
+ ---
2
+ id: DOCS-008
3
+ status: accepted
4
+ ---
5
+
6
+ # DOCS-008 — #320 obligation abstraction as a bounded doc consumer
7
+
8
+ ## Decision
9
+
10
+ - The doc workflow reconciliation consumes the workflow obligation abstraction in a bounded way: the documentation path only, under the DOCS-006 gate.
11
+ - Broad promotion of the abstraction to generic workflow use is not approved. It is gated on the Phase 3c H1–H4 paired-task evidence and owner sign-off.
12
+
13
+ ## Context
14
+
15
+ | Issue | Subject | State |
16
+ | --- | --- | --- |
17
+ | #320 | Compile workflow/skill metadata into a dynamic decision graph | OPEN |
18
+
19
+ - Phase 3c is planned, not shipped: [document-lifecycle.md § Phase 3c](../document-lifecycle.md#phase-3c--architecture-promotion-gate-for-320-planned-not-shipped).
20
+
21
+ ## Evidence
22
+
23
+ - Implementation: [doc-reconciliation-core.js](../../hooks/scripts/lib/doc-reconciliation-core.js), [doc-obligations.js](../../harness-everything/scripts/doc-obligations.js) (the obligation table consumed by the doc path).
24
+ - Consumer tests (exercise `doc-reconciliation-core.js` consuming `deriveDocObligations`): [mechanism-325-3b-s2a-core.test.js](../../ci/mechanism-325-3b-s2a-core.test.js), [mechanism-325-3b-s2b-wiring.test.js](../../ci/mechanism-325-3b-s2b-wiring.test.js).
25
+ - Fail-closed controls (not evidence of the consumer): [mechanism-325-3b-preparation.test.js](../../ci/mechanism-325-3b-preparation.test.js), [mechanism-325-3b-s1-review.test.js](../../ci/mechanism-325-3b-s1-review.test.js), [mechanism-341-opencode-lane.test.js](../../ci/mechanism-341-opencode-lane.test.js).
26
+
27
+ ## Open gates
28
+
29
+ - #320 OPEN. Its own promotion gate is unmet: H1–H4 paired-task evidence has not been collected.
30
+ - Pilot gate (#341 E1) applies, see DOCS-006.
@@ -1213,3 +1213,33 @@ claude -p "<fixed capture prompt, containing the exact check command>" \
1213
1213
  - **The live gate is a repository-pinned field-availability check, not a running-host compatibility check (F10).** The qualification export records Claude Code `2.1.308`, but the retained Claude hook payloads expose no host-version field for the runtime to compare. The export and sibling sanitized capture validate the recorded payload kinds and correlations for that host version; they do not establish compatibility with the current session. Editing both repository artifacts and updating the capture digest can reopen the gate without a new live capture.
1214
1214
  - **The unsupported-host check is vacuous in production (F11).** The only production caller asserts the Claude host id. OpenCode and Codex stay unavailable because their plugins do not mount `state-persist.js`, not because the host is detected.
1215
1215
  - **Event files are not tamper-proof (F13).** Event files are created exclusively, and the journal is checked against them. The local agent can still rewrite event files and rebuild the journal. This is the owner's bounded claim, not a guarantee of tamper resistance.
1216
+
1217
+ ## PR-B: dedicated pre-draft outline confirmation
1218
+
1219
+ Status: **implemented behind the existing pilot switch; mechanism and package checks pass; no positive live-host credit is enabled.** This capability follows owner decisions 9 and 11: an independent reviewer approves exact O before drafting; the outline cites one row from the pinned inventory; final D is a strict descendant of O; a coordinator handoff records review-complete → fresh-draft-dispatch. Owner approval is provenance only and never a runtime reviewer principal.
1220
+
1221
+ ```mermaid
1222
+ flowchart LR
1223
+ B["Original baseline B<br/>task and config pinned"] --> O["Proposed outline O<br/>new target absent at B"]
1224
+ I["Coordinator-pinned inventory"] --> R["Independent review<br/>exact O + cited row"]
1225
+ O --> R
1226
+ R --> H["Immutable handoff +<br/>exclusive writer lease"]
1227
+ H --> P["Task/Agent PreToolUse<br/>exact prompt marker"]
1228
+ P --> S["Synchronous seam<br/>clean worker root at O<br/>one-use token consumed"]
1229
+ S --> W["Correlated worker start/result"]
1230
+ W --> D["Final candidate D<br/>strict descendant of O"]
1231
+ D --> A["Outline result and<br/>aggregate B→D MUSTs"]
1232
+ ```
1233
+
1234
+ | Evidence | Required binding |
1235
+ |---|---|
1236
+ | B and task | Keep the workflow-start B commit/root/tree and ordered requirement IDs for every attempt. Config scope must still match B. An attempt cannot reset B to O. |
1237
+ | Inventory | The coordinator supplies exact bytes and `inventory-pin.json` outside the candidate tree. The pinned source is `report-inv.md`, SHA-256 `2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6`, run `e1-inv-1`, input SHA `0ef0541c6cae550e8269ba712e03797e0e987dfb`, approval `#325` comment `6436207729`, decision 1. The provider rehashes the bytes and compares every pin field before resolving one row and domain. Missing, stale, duplicate, ambiguous, wrong-target or unavailable evidence is unresolved with no fallback. Synthetic test pins prove parser/mechanism binding only. |
1238
+ | O and review | O is bound to its commit, tree, target blobs and stored-byte digests. Its `proposed` ADR frontmatter cites one `Inventory row: <id>`, target and domain, and contains no completed Decision text. A host-captured `SubagentStop` review must approve those exact O bytes and the pinned inventory, with a distinct host `agent_id` and correlated Agent/Task result. |
1239
+ | Handoff and dispatch | One immutable attempt handoff binds B, O, scope, inventory, accepted review, coordinator principal, lease and one-use token. Agent/Task dispatch accepts exactly one `HARNESS_OUTLINE_DISPATCH_V1` block in `tool_input.prompt`, whose token must equal the handoff token; it does not search free-form text or use another field or cwd as a fallback. Before worker start, PreToolUse verifies the handoff, review, pin, exclusive lease, canonical root, HEAD/tree at O and clean index/worktree including untracked files; it creates a write-once handoff-digest seal, stores validation digests/reasons, then consumes the token by exclusive creation. Every predecessor seal, seam and token remains bound to the original handoff bytes, so an overwritten predecessor invalidates the chain. |
1240
+ | Worker and D | Worker start must correlate to the dispatch token/tool ID, principal and declared root; the eventual Task/Agent result must name that same worker. D is checked against the current candidate separately from O. O must be a strict ancestor of D. A squash may retain the cycle only when a coordinator correspondence record proves identical target blobs between retained pre-merge D and the merged tree; changed bytes need fresh final quality review. |
1241
+ | Checker outcome | `outline-confirmation` has its own pass/unresolved lane. Generic command success and waivers never satisfy it. No D is incomplete/unresolved. Its result is separate from the aggregate result over every MUST derived from original B→D; an outline pass never waives lint, conflict, quality review, requirement-sync or other MUSTs. |
1242
+
1243
+ Capture and validation failures stay fail-open to host tools and withhold evidence credit. Unclassified shell commands and writes outside the correlated worker/root remain unresolved. Retries use a fresh attempt, token, clean worktree and immutable handoff; only one non-superseded leaf is current. Exact same-tool-ID/input duplicates may resolve to one seam; changed input or token reuse is unresolved. OpenCode and Codex remain unsupported for positive outline credit.
1244
+
1245
+ The current retained Claude qualification does not establish the frozen prompt field, coordinator principal, synchronous PreToolUse boundary or worker-root mapping. The outline provider therefore grants no positive live-host credit until a retained Claude capture validates those mappings. Deterministic fixtures exercise the real hook/provider/adapter/checker/pilot path with a synthetic inventory pin; they are mechanism evidence, not live-host behavior or E1 readiness.
@@ -4,6 +4,14 @@
4
4
 
5
5
  - `(no id)` — `docs/architecture.md` (role: reference; status: active)
6
6
  - `(no id)` — `docs/doc-workflow-reconciliation.md` (role: reference; status: active)
7
+ - `DOCS-001` — `docs/adr/DOCS-001-project-docs-config.md` (role: adr; status: accepted)
8
+ - `DOCS-002` — `docs/adr/DOCS-002-adr-identity-supersession-index.md` (role: adr; status: accepted)
9
+ - `DOCS-003` — `docs/adr/DOCS-003-prd-revision-contract.md` (role: adr; status: accepted)
10
+ - `DOCS-004` — `docs/adr/DOCS-004-doc-change-step-type.md` (role: adr; status: accepted)
11
+ - `DOCS-005` — `docs/adr/DOCS-005-doc-obligations-evidence-producer.md` (role: adr; status: accepted)
12
+ - `DOCS-006` — `docs/adr/DOCS-006-reconciliation-pilot.md` (role: adr; status: accepted)
13
+ - `DOCS-007` — `docs/adr/DOCS-007-shared-doc-craft-derived-views.md` (role: adr; status: accepted)
14
+ - `DOCS-008` — `docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md` (role: adr; status: accepted)
7
15
 
8
16
  ## PRD revision chains
9
17
 
@@ -210,6 +210,8 @@ The editorial doc check (`harness-everything/scripts/doc-check.js`, F1) is reach
210
210
 
211
211
  Credited reconciliation for F4 (Claude Code only, pilot ON) is **implemented, with mechanism and package evidence and one live field-availability capture**. Credit works for `doc-lint` and `link-check` through a registered `doc-check` run, and for an independent `quality-review` with bound path digests. The capture under `benchmarks/results/live-host/claude-325-3b-s2b/` proves field availability on Claude Code 2.1.308 for one session. It is not live-host credit, and no live-host credit run exists yet. Deterministic mechanism tests (replay through the real hook entry points, with a CI-only qualification preloader) prove the chain.
212
212
 
213
+ PR-B adds a dedicated `outline-confirmation` evidence lane for Claude captures with separate B/O/D checks, a coordinator-sealed pre-dispatch handoff, pinned-inventory row binding, and an independent exact-O review. It is **mechanism and package evidence only**. The retained qualification does not yet establish `tool_input.prompt`, the coordinator identity, synchronous PreToolUse boundary or worker-root mapping, so positive live-host outline credit remains disabled. OpenCode and Codex receive no outline credit; missing or ambiguous capture stays unresolved and host tools remain fail-open. The outline result does not replace the aggregate B→D MUST result or the existing exact-D `quality-review` contract.
214
+
213
215
  ### Own-repository governance guard (PR #376)
214
216
 
215
217
  On `pull_request`, a standalone `doc-governance` job extracts `scripts/doc-governance-guard.js` from the event base commit and runs those bytes against the exact event base/head SHAs. Its nonzero result fails that job; `self-regression` and the other CI jobs run independently. The ordinary test suite may still run on GitHub's PR checkout SHA, which can be a synthetic merge commit; it is reported separately. The guard never falls back to the candidate copy. PR #376's reviewed base lacked the verifier; that historical bootstrap reported BOOTSTRAP_UNCHECKED without executing candidate bytes. Bases containing the guard (including main at 0ef0541c) execute the base-owned verifier. If a selected PR base lacks the guard, the wrapper retains the warning/step-summary-only bootstrap path with exit 0; this is unverified governance, not PASS. Every nonzero exit from an extracted verifier, including FAIL and BOOTSTRAP_UNCHECKED, fails the governance job. Other CI jobs run independently.
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.45.0
7
+ version: 0.46.0
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -433,6 +433,61 @@ function evaluateObligation(obligation, context) {
433
433
  return entry ? baselineBound(item, entry) : candidateBound(item, path);
434
434
  };
435
435
 
436
+ if (capability === 'outline-confirmation') {
437
+ const candidates = matching.filter(item => reviewList.includes(item) && item.kind === 'outline-confirmation');
438
+ if (candidates.length !== 1) {
439
+ return result('unresolved', 'none',
440
+ 'outline-confirmation requires one validated pre-draft O/review/handoff/dispatch record and a final D; generic execution and waivers do not qualify',
441
+ candidates.map(item => item.id));
442
+ }
443
+ const item = candidates[0];
444
+ const evidence = isPlainObject(item.outlineEvidence) ? item.outlineEvidence : null;
445
+ const sha = value => isSha256(value);
446
+ const digestsMatch = map => isPlainObject(map)
447
+ && Object.keys(map).sort(compareText).join('\0') === scopePaths.slice().sort(compareText).join('\0')
448
+ && scopePaths.every(path => sha(map[path]));
449
+ const identityValid = Boolean(evidence)
450
+ && evidence.schema === 'harness.outline-confirmation.v1' && evidence.state === 'validated'
451
+ && nonEmptyString(evidence.taskId) && nonEmptyString(evidence.attemptId)
452
+ && /^\d{1,2}$/.test(String(evidence.inventoryRowId || ''))
453
+ && /^[A-Z][A-Z0-9]{1,9}$/.test(String(evidence.domain || ''))
454
+ && sha(evidence.inventoryPinDigest) && sha(evidence.inventoryArtifactDigest)
455
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.baselineCommit || ''))
456
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.outlineCommit || ''))
457
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.draftCommit || ''))
458
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.mergedCommit || ''))
459
+ && evidence.outlineCommit !== evidence.draftCommit
460
+ && evidence.ancestry === 'strict-descendant'
461
+ && sha(evidence.handoffDigest) && sha(evidence.reviewCaptureDigest) && sha(evidence.reviewDigest)
462
+ && sha(evidence.seamDigest) && sha(evidence.dispatchInputDigest) && sha(evidence.writerLeaseDigest)
463
+ && nonEmptyString(evidence.dispatchToolUseId) && nonEmptyString(evidence.coordinatorPrincipal)
464
+ && nonEmptyString(evidence.reviewerPrincipal) && nonEmptyString(evidence.workerPrincipal)
465
+ && evidence.reviewerPrincipal !== evidence.coordinatorPrincipal
466
+ && evidence.reviewerPrincipal !== evidence.workerPrincipal
467
+ && Array.isArray(evidence.targetPaths)
468
+ && evidence.targetPaths.slice().sort(compareText).join('\0') === scopePaths.slice().sort(compareText).join('\0')
469
+ && digestsMatch(evidence.outlineArtifactDigests) && digestsMatch(evidence.draftArtifactDigests)
470
+ && digestsMatch(item.artifactDigests)
471
+ && scopePaths.every(path => evidence.draftArtifactDigests[path] === item.artifactDigests[path]
472
+ && candidateBound(item, path));
473
+ const mergeValid = evidence && (evidence.mergeCorrespondence === null
474
+ || (isPlainObject(evidence.mergeCorrespondence)
475
+ && ['accepted', 'changed'].includes(evidence.mergeCorrespondence.state)
476
+ && Array.isArray(evidence.mergeCorrespondence.targetPaths)
477
+ && evidence.mergeCorrespondence.targetPaths.slice().sort(compareText).join('\0')
478
+ === scopePaths.slice().sort(compareText).join('\0')));
479
+ const outlineProblems = [];
480
+ if (!identityValid) outlineProblems.push('envelope-or-current-D-binding');
481
+ if (!mergeValid) outlineProblems.push('merge-correspondence');
482
+ if (item.baseDigest !== baselineDigest) outlineProblems.push('original-B-digest');
483
+ if (outlineProblems.length) {
484
+ diagnostics.push({ code: 'outline-evidence-unresolved', obligationId,
485
+ message: `the outline evidence is incomplete or inconsistent: ${outlineProblems.join(', ')}` });
486
+ return result('unresolved', 'none', 'outline evidence is incomplete or unbound to the original B and final D', [item.id]);
487
+ }
488
+ return result('observed', 'pass', 'validated independent review of exact O, coordinator handoff, clean one-use dispatch, pinned inventory, and strict O-to-D evidence', [item.id]);
489
+ }
490
+
436
491
  for (const item of matching) {
437
492
  if (executionList.includes(item) && !completeExecution(item)) {
438
493
  diagnostics.push({
package/hooks/hooks.json CHANGED
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "PreToolUse": [
39
39
  {
40
- "matcher": "Bash|PowerShell|Task|Agent",
40
+ "matcher": "Bash|PowerShell|Task|Agent|Write|Edit|apply_patch",
41
41
  "hooks": [
42
42
  {
43
43
  "type": "command",
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const path = require('path');
4
+ const { execFileSync } = require('child_process');
5
5
  const { _internals: U } = require('./doc-reconciliation-core');
6
+ const { key } = require('./workflow-isolation');
6
7
 
7
8
  // Directory identity as in the producer: native realpath (Windows 8.3 names), case-insensitive on Windows.
8
9
  function samePath(a, b) {
9
10
  try {
10
- const left = fs.realpathSync.native(path.resolve(a));
11
- const right = fs.realpathSync.native(path.resolve(b));
12
- return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
11
+ fs.statSync(a);
12
+ fs.statSync(b);
13
+ return key(a) === key(b);
13
14
  } catch { return false; }
14
15
  }
15
16
 
@@ -19,6 +20,109 @@ function artifactsFor(records) {
19
20
  function prdDigest(artifacts) {
20
21
  return U.sha(JSON.stringify(Object.keys(artifacts).filter(p => p.startsWith('docs/prd/')).sort().map(p => [p, artifacts[p].digest])));
21
22
  }
23
+
24
+ function git(root, args, binary = false) {
25
+ const env = { ...process.env };
26
+ for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
27
+ return execFileSync('git', ['-C', root, ...args], { env, encoding: binary ? null : 'utf8',
28
+ timeout: 10000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
29
+ }
30
+
31
+ function shaBytes(value) { return U.sha(Buffer.from(value)); }
32
+
33
+ function outlineEventValid({ event, snapshot, paths, artifacts, baselineArtifacts } = {}) {
34
+ let stage = 'envelope';
35
+ try {
36
+ const evidence = event?.outlineEvidence;
37
+ const root = snapshot.originContext.workspaceRoot;
38
+ const baseline = snapshot.originContext.baseline;
39
+ if (snapshot.originContext.activeHost !== 'claude-code' || !U.record(evidence)
40
+ || evidence.schema !== 'harness.outline-confirmation.v1' || evidence.state !== 'validated'
41
+ || evidence.baselineCommit !== baseline?.commit || evidence.taskId !== snapshot.identity.taskId
42
+ || !/^[0-9a-f]{40,64}$/.test(String(evidence.outlineCommit || ''))
43
+ || !/^[0-9a-f]{40,64}$/.test(String(evidence.draftCommit || ''))
44
+ || !/^[0-9a-f]{40,64}$/.test(String(evidence.mergedCommit || ''))
45
+ || !/^[0-9a-f]{64}$/.test(String(evidence.handoffDigest || ''))
46
+ || !/^[0-9a-f]{64}$/.test(String(evidence.reviewCaptureDigest || ''))
47
+ || !/^[0-9a-f]{64}$/.test(String(evidence.reviewDigest || ''))
48
+ || !/^[0-9a-f]{64}$/.test(String(evidence.seamDigest || ''))
49
+ || !/^[0-9a-f]{64}$/.test(String(evidence.dispatchInputDigest || ''))
50
+ || !/^[0-9a-f]{64}$/.test(String(evidence.writerLeaseDigest || ''))
51
+ || !/^[0-9a-f]{64}$/.test(String(evidence.inventoryPinDigest || ''))
52
+ || !/^[0-9a-f]{64}$/.test(String(evidence.inventoryArtifactDigest || ''))
53
+ || !Array.isArray(evidence.targetPaths) || U.stable(evidence.targetPaths) !== U.stable(paths)
54
+ || !U.record(evidence.outlineArtifactDigests) || !U.record(evidence.draftArtifactDigests)
55
+ || !U.record(evidence.outlineBlobOids) || !U.record(evidence.draftBlobOids)
56
+ || U.stable(Object.keys(evidence.outlineArtifactDigests).sort()) !== U.stable(paths)
57
+ || U.stable(Object.keys(evidence.draftArtifactDigests).sort()) !== U.stable(paths)
58
+ || U.stable(Object.keys(evidence.outlineBlobOids).sort()) !== U.stable(paths)
59
+ || U.stable(Object.keys(evidence.draftBlobOids).sort()) !== U.stable(paths)
60
+ || !U.record(event.draftArtifactDigests) || U.stable(Object.keys(event.draftArtifactDigests).sort()) !== U.stable(paths)
61
+ || U.stable(event.draftArtifactDigests) !== U.stable(evidence.draftArtifactDigests)
62
+ || !U.record(event.reviewedArtifactDigests)
63
+ || !paths.every(p => /^[0-9a-f]{64}$/.test(String(evidence.outlineArtifactDigests[p] || ''))
64
+ && /^[0-9a-f]{64}$/.test(String(evidence.draftArtifactDigests[p] || ''))
65
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.outlineBlobOids[p] || ''))
66
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.draftBlobOids[p] || ''))
67
+ && artifacts[p]?.digest === evidence.draftArtifactDigests[p])
68
+ || !/^[0-9a-f]{40,64}$/.test(String(evidence.baselineCommit || ''))
69
+ || !/^[0-9a-f]{40,64}$/.test(String(evidence.outlineTree || ''))
70
+ || !/^[0-9a-f]{40,64}$/.test(String(evidence.draftTree || ''))
71
+ || !U.text(evidence.coordinatorPrincipal) || !U.text(evidence.reviewerPrincipal) || !U.text(evidence.workerPrincipal)
72
+ || evidence.reviewerPrincipal === evidence.coordinatorPrincipal || evidence.reviewerPrincipal === evidence.workerPrincipal
73
+ || evidence.ancestry !== 'strict-descendant' || evidence.outlineCommit === evidence.draftCommit) return stage;
74
+
75
+ stage = 'reviewed-object-binding';
76
+ const pinPath = Object.keys(event.reviewedArtifactDigests).filter(p => p.startsWith('doc-reconciliation/outline-confirmation/inventory/'));
77
+ if (pinPath.length !== 1 || event.reviewedArtifactDigests[pinPath[0]] !== evidence.inventoryArtifactDigest
78
+ || Object.keys(event.reviewedArtifactDigests).length !== paths.length + 1
79
+ || !paths.every(p => event.reviewedArtifactDigests[p] === evidence.outlineArtifactDigests[p])) return stage;
80
+
81
+ stage = 'current-commit-binding';
82
+ const head = git(root, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
83
+ const tree = git(root, ['rev-parse', '--verify', 'HEAD^{tree}']).trim();
84
+ if (head !== evidence.mergedCommit || tree !== evidence.draftTree) return stage;
85
+ const verified = commit => git(root, ['rev-parse', '--verify', `${commit}^{commit}`]).trim() === commit;
86
+ if (![evidence.baselineCommit, evidence.outlineCommit, evidence.draftCommit, evidence.mergedCommit].every(verified)) return 'git-object-missing';
87
+ if (evidence.baselineCommit === evidence.outlineCommit) return 'B-to-O-ancestry';
88
+ stage = 'B-to-O-ancestry';
89
+ try { git(root, ['merge-base', '--is-ancestor', evidence.baselineCommit, evidence.outlineCommit]); }
90
+ catch { return stage; }
91
+ stage = 'O-to-D-ancestry';
92
+ try { git(root, ['merge-base', '--is-ancestor', evidence.outlineCommit, evidence.draftCommit]); }
93
+ catch { return stage; }
94
+ stage = 'merge-correspondence';
95
+ if (evidence.mergeCorrespondence === null) {
96
+ if (evidence.draftCommit !== evidence.mergedCommit) return stage;
97
+ } else {
98
+ const merge = evidence.mergeCorrespondence;
99
+ if (!U.record(merge) || !['accepted', 'changed'].includes(merge.state)
100
+ || merge.draftCommit !== evidence.draftCommit || merge.mergedCommit !== evidence.mergedCommit
101
+ || U.stable(merge.targetPaths) !== U.stable(paths) || !U.record(merge.targetBlobs)) return stage;
102
+ for (const p of paths) {
103
+ const draftOid = git(root, ['rev-parse', `${evidence.draftCommit}:${p}`]).trim();
104
+ const mergedOid = git(root, ['rev-parse', `${evidence.mergedCommit}:${p}`]).trim();
105
+ if (merge.targetBlobs[p]?.draft !== draftOid || merge.targetBlobs[p]?.merged !== mergedOid
106
+ || (merge.state === 'accepted' && draftOid !== mergedOid)
107
+ || (merge.state === 'changed' && draftOid === mergedOid)) return stage;
108
+ }
109
+ }
110
+ stage = 'target-object-binding';
111
+ for (const p of paths) {
112
+ const atBase = git(root, ['ls-tree', '-r', '--name-only', evidence.baselineCommit, '--', p]).trim().split('\n').filter(Boolean);
113
+ if (atBase.includes(p)) return 'target-existed-at-B';
114
+ if (git(root, ['rev-parse', `${evidence.outlineCommit}:${p}`]).trim() !== evidence.outlineBlobOids[p]
115
+ || git(root, ['rev-parse', `${evidence.mergedCommit}:${p}`]).trim() !== evidence.draftBlobOids[p]) return stage;
116
+ const outlineBytes = git(root, ['show', `${evidence.outlineCommit}:${p}`], true);
117
+ const draftBytes = git(root, ['show', `${evidence.mergedCommit}:${p}`], true);
118
+ if (shaBytes(outlineBytes) !== evidence.outlineArtifactDigests[p]
119
+ || shaBytes(draftBytes) !== evidence.draftArtifactDigests[p]
120
+ || shaBytes(draftBytes) !== artifacts[p]?.digest) return 'candidate-artifact-binding';
121
+ }
122
+ return true;
123
+ } catch { return 'git-verification-failed'; }
124
+ }
125
+
22
126
  function acceptDocEvents(input = {}, dependencies = {}) {
23
127
  const { snapshot, eventRefs } = U.record(input) ? input : {};
24
128
  const originProvider = dependencies?.originProvider;
@@ -91,8 +195,12 @@ function acceptDocEvents(input = {}, dependencies = {}) {
91
195
  seenCapabilities.add(event?.capability);
92
196
  const paths = obligation?.scope?.paths || [];
93
197
  if (!Array.isArray(event?.paths) || U.stable([...event.paths].sort()) !== U.stable([...paths].sort())) reject('scope-mismatch');
198
+ const outlineCapability = event?.capability === 'outline-confirmation';
94
199
  const digests = event?.kind === 'review' ? event.reviewedArtifactDigests : event?.artifactDigests;
95
- if (!U.record(digests)) reject('artifact-mismatch');
200
+ if (outlineCapability) {
201
+ const outlineValidation = outlineEventValid({ event, snapshot, paths, artifacts, baselineArtifacts });
202
+ if (outlineValidation !== true) reject(`outline-${outlineValidation || 'evidence-invalid'}`);
203
+ } else if (!U.record(digests)) reject('artifact-mismatch');
96
204
  else {
97
205
  for (const p of paths) {
98
206
  if (artifacts[p] && digests[p] !== artifacts[p].digest) reject('artifact-mismatch');
@@ -135,10 +243,13 @@ function acceptDocEvents(input = {}, dependencies = {}) {
135
243
  if (!context?.principals?.executors?.includes(event?.principalId)) reject('origin-untrusted');
136
244
  }
137
245
  if (problems.length) { rejected.push(...problems); continue; }
138
- const adapted = { id: reference, obligationId, kind: event.kind === 'review' ? 'review' : 'execution',
246
+ const adapted = { id: reference, obligationId,
247
+ kind: outlineCapability ? 'outline-confirmation' : event.kind === 'review' ? 'review' : 'execution',
139
248
  paths: [...paths], provenance: event.provenance, baseDigest: snapshot.identity.baselineDigest,
140
- artifactDigests: U.clone(digests), ...(event.baselineArtifacts ? { baselineArtifacts: U.clone(event.baselineArtifacts) } : {}) };
141
- if (event.kind === 'review') Object.assign(adapted, { verdict: event.verdict, summary: event.summary });
249
+ artifactDigests: U.clone(outlineCapability ? event.draftArtifactDigests : digests),
250
+ ...(event.baselineArtifacts ? { baselineArtifacts: U.clone(event.baselineArtifacts) } : {}) };
251
+ if (outlineCapability) Object.assign(adapted, { outlineEvidence: U.clone(event.outlineEvidence) });
252
+ else if (event.kind === 'review') Object.assign(adapted, { verdict: event.verdict, summary: event.summary });
142
253
  else Object.assign(adapted, { command: event.command, exitCode: event.exitCode });
143
254
  if (event.capability === 'conflict-check') {
144
255
  adapted.candidatePrdSet = { digest: prdDigest(artifacts), complete: true, paths: Object.keys(artifacts).filter(p => p.startsWith('docs/prd/')).sort() };
@@ -146,7 +257,8 @@ function acceptDocEvents(input = {}, dependencies = {}) {
146
257
  (event.kind === 'review' ? checkerInputs.reviewRecords : checkerInputs.executionRecords).push(adapted);
147
258
  acceptedIndex[reference] = { reference, eventId: event.eventId, obligationId, capability: event.capability,
148
259
  paths: [...paths], identity: U.clone(event.identity), originRef: response.originRef, artifactBinding: snapshot.identity.candidateDigest,
149
- artifactDigests: U.clone(digests), event };
260
+ artifactDigests: U.clone(outlineCapability ? event.draftArtifactDigests : digests),
261
+ ...(outlineCapability ? { reviewedArtifactDigests: U.clone(event.reviewedArtifactDigests) } : {}), event };
150
262
  }
151
263
  if (diagnostics.length || rejected.length) out.state = 'unresolved';
152
264
  return out;