hadara 0.3.0-rc.2 → 0.3.1-rc.1

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.
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- <img alt="Published npm release" src="https://img.shields.io/badge/npm-0.3.0--rc.1-blue">
9
- <img alt="Source candidate" src="https://img.shields.io/badge/source-0.3.0--rc.2-orange">
8
+ <img alt="Stable npm release" src="https://img.shields.io/badge/npm-0.3.0-blue">
9
+ <img alt="Source version" src="https://img.shields.io/badge/source-0.3.1--rc.1-orange">
10
10
  <img alt="Node.js" src="https://img.shields.io/badge/node-%3E%3D22-brightgreen">
11
11
  <img alt="License" src="https://img.shields.io/badge/license-MIT-lightgrey">
12
12
  </p>
@@ -21,37 +21,28 @@ This repository is both the HADARA source checkout and the HADARA protocol works
21
21
 
22
22
  ## Release Status
23
23
 
24
- Current published npm release:
24
+ Current stable npm release:
25
25
 
26
26
  ```text
27
- hadara@0.3.0-rc.1
27
+ hadara@0.3.0
28
28
  ```
29
29
 
30
- Current source release candidate:
30
+ Current release candidate:
31
31
 
32
32
  ```text
33
- hadara@0.3.0-rc.2
33
+ hadara@0.3.1-rc.1
34
34
  ```
35
35
 
36
- Previous published npm release:
36
+ The 0.3.1-rc.1 line is the Phase 8 State Governance release candidate. It hardens status-token vocabulary, document ownership/write boundaries, handoff close-state handling, installed-package finding guidance, and state consistency verification surfaces after the stable 0.3.0 publish/recycle.
37
37
 
38
- ```text
39
- hadara@0.3.0-rc.0
40
- ```
41
-
42
- The 0.3.0 line is the Phase 7 Surface Refactor. It organizes HADARA's existing task, evidence, proof, lifecycle, release, and document surfaces so agents can distinguish primary lifecycle commands, diagnostics, advanced surfaces, canonical documents, historical documents, and safe Markdown update boundaries.
43
-
44
- Phase 7.x labels are internal implementation phases, not npm release-candidate labels. Publishing any later release still requires the approval-gated release path and explicit operator confirmation.
38
+ Phase 8.x labels are internal implementation phases, not npm release-candidate labels. The stable `0.3.0` package remains the default install target; the `0.3.1-rc.1` package is for explicit release-candidate evaluation.
45
39
 
46
40
  | Surface | Status |
47
41
  |---|---|
48
- | npm package | Primary release target. |
49
- | `hadara@0.2.0-rc.1` | Previous published npm RC. |
50
- | `hadara@0.2.0-rc.2` | Previous published npm RC. |
51
- | `hadara@0.2.0-rc.3` | Previous published npm RC. |
52
- | `hadara@0.3.0-rc.0` | Previous published npm RC; package metadata lacks the intended discovery fields. |
53
- | `hadara@0.3.0-rc.1` | Current published npm RC; T-0301 publish evidence verified `npm view` returned `0.3.0-rc.1`. |
54
- | `hadara@0.3.0-rc.2` | Current source candidate prepared by T-0310; publish remains approval-gated. |
42
+ | Current stable | [`hadara@0.3.0`](docs/RELEASE_NOTES.md#030) |
43
+ | Current RC | [`hadara@0.3.1-rc.1`](docs/RELEASE_NOTES.md#031-rc1) |
44
+ | Previous RC | [`hadara@0.3.0-rc.2`](docs/RELEASE_NOTES.md#030-rc2) |
45
+ | Historical RCs | See [Release Notes](docs/RELEASE_NOTES.md). |
55
46
  | GitHub Release | Secondary target, approval-gated. |
56
47
  | Docker image | Deferred. |
57
48
  | PyPI/Python package | `hadara==0.2.0rc1` published preview bridge. |
@@ -63,10 +54,10 @@ No release command should publish, create a GitHub Release, build Docker images,
63
54
 
64
55
  Requires Node.js 22.
65
56
 
66
- Install the latest published RC:
57
+ Install the stable release:
67
58
 
68
59
  ```bash
69
- npm install -g hadara@0.3.0-rc.1
60
+ npm install -g hadara@0.3.0
70
61
  hadara help
71
62
  hadara doctor --json
72
63
  ```
@@ -74,10 +65,27 @@ hadara doctor --json
74
65
  Run without a global install:
75
66
 
76
67
  ```bash
77
- npx hadara@0.3.0-rc.1 help
78
- npx hadara@0.3.0-rc.1 doctor --json
68
+ npx hadara@0.3.0 help
69
+ npx hadara@0.3.0 doctor --json
79
70
  ```
80
71
 
72
+ Evaluate the release candidate explicitly:
73
+
74
+ ```bash
75
+ npm install -g hadara@0.3.1-rc.1
76
+ npx hadara@0.3.1-rc.1 help
77
+ ```
78
+
79
+ For release or recycle evidence, prefer an isolated prefix install when PATH, global installs, or `npx` cache behavior may be stale:
80
+
81
+ ```bash
82
+ tmp="$(mktemp -d)"
83
+ npm --prefix "$tmp" install hadara@0.3.0
84
+ "$tmp/node_modules/.bin/hadara" version --json
85
+ ```
86
+
87
+ `npx hadara@0.3.0 ...` remains convenient for normal use. The isolated installed-bin path is stronger proof that the published package installed and executed from the intended package tree.
88
+
81
89
  ## What HADARA Gives You
82
90
 
83
91
  | Capability | Purpose |
@@ -224,7 +232,7 @@ Dashboard, TUI, Hermes, MCP, installer, package, release, and run commands stay
224
232
 
225
233
  ## Safety Boundaries
226
234
 
227
- HADARA 0.3.0 is not:
235
+ HADARA 0.3.1-rc.1 is not:
228
236
 
229
237
  - a full agent runtime;
230
238
  - Rack/enterprise behavior;
@@ -297,19 +305,6 @@ node dist/cli/main.js doctor --json
297
305
  npm run check
298
306
  ```
299
307
 
300
- For HADARA-dev itself, Docker is the preferred validation path because host `node_modules` on mounted workspaces can be unreliable:
301
-
302
- ```bash
303
- npm run dev:docker-check
304
- npm run dev:docker-sync-build
305
- ```
306
-
307
- Focused validation:
308
-
309
- ```bash
310
- npm run test:focused -- tests/unit/release-dry-run.test.ts
311
- ```
312
-
313
308
  Optional integrations are not generated by `hadara init` and are not part of the default scaffold:
314
309
 
315
310
  ```bash
package/dist/cli/init.js CHANGED
@@ -571,7 +571,7 @@ function sopRequiredReadingRowsForProfile(profile) {
571
571
  rows.push(['`docs/ARCHITECTURE.md`', 'Architecture, component, or boundary work', 'Current system shape and ownership boundaries.'], ['`docs/DEVELOPMENT_SLICES.md`', 'Starting, completing, or reclassifying slices', 'Roadmap ordering and completion evidence.'], ['`docs/DECISIONS.md`', 'Project-level decision work', 'Durable decisions that affect architecture or workflow.'], ['`docs/TEST_STRATEGY.md`', 'Validation planning or completion checks', 'Routine suites and special-case smoke boundaries.']);
572
572
  }
573
573
  if (profile === 'governed') {
574
- rows.push(['`docs/SECURITY_MODEL.md`', 'Security, secret, permission, or evidence-safety work', 'Project security invariants and special checks.'], ['`docs/REFACTOR_LOG.md`', 'Refactor, migration, removal, or replacement work', 'Project-level refactor history.'], ['`docs/ROADMAP.md`', 'Roadmap, milestone, or scope planning', 'Longer-term priorities and deferred work.']);
574
+ rows.push(['`docs/SECURITY_MODEL.md`', 'Security, secret, permission, or evidence-safety work', 'Project security invariants and special checks.'], ['`docs/ROADMAP.md`', 'Roadmap, milestone, or scope planning', 'Longer-term priorities and deferred work.']);
575
575
  }
576
576
  return rows;
577
577
  }
@@ -587,7 +587,7 @@ function agentsRequiredReadingRowsForProfile(profile) {
587
587
  rows.push({ document: '`docs/ARCHITECTURE.md`', when: 'Architecture, component, or boundary work', purpose: 'Current system shape and ownership boundaries.' }, { document: '`docs/DEVELOPMENT_SLICES.md`', when: 'Starting, completing, or reclassifying a development slice', purpose: 'Roadmap ordering, prerequisites, and completion evidence.' }, { document: '`docs/DECISIONS.md`', when: 'Project-level decision work', purpose: 'Durable project decisions.' }, { document: '`docs/TEST_STRATEGY.md`', when: 'Validation planning or completion checks', purpose: 'Routine suites and special-case checks.' });
588
588
  }
589
589
  if (profile === 'governed') {
590
- rows.push({ document: '`docs/SECURITY_MODEL.md`', when: 'Security, secret, permission, or evidence-safety work', purpose: 'Project security invariants.' }, { document: '`docs/REFACTOR_LOG.md`', when: 'Refactor, migration, removal, or replacement work', purpose: 'Project-level refactor history.' }, { document: '`docs/ROADMAP.md`', when: 'Roadmap, milestone, or scope planning', purpose: 'Longer-term priorities and deferred work.' });
590
+ rows.push({ document: '`docs/SECURITY_MODEL.md`', when: 'Security, secret, permission, or evidence-safety work', purpose: 'Project security invariants.' }, { document: '`docs/ROADMAP.md`', when: 'Roadmap, milestone, or scope planning', purpose: 'Longer-term priorities and deferred work.' });
591
591
  }
592
592
  return rows;
593
593
  }
@@ -710,7 +710,7 @@ function requiredDocsForProfile(profile) {
710
710
  docs.push('docs/ARCHITECTURE.md', 'docs/DEVELOPMENT_SLICES.md', 'docs/DECISIONS.md', 'docs/TEST_STRATEGY.md');
711
711
  }
712
712
  if (profile === 'governed') {
713
- docs.push('docs/SECURITY_MODEL.md', 'docs/REFACTOR_LOG.md', 'docs/ROADMAP.md');
713
+ docs.push('docs/SECURITY_MODEL.md', 'docs/ROADMAP.md');
714
714
  }
715
715
  return docs;
716
716
  }
@@ -973,9 +973,6 @@ function createImplementationSopDoc(spec) {
973
973
  if (spec.docs.securityModel) {
974
974
  requiredReadingRows.push(['`docs/SECURITY_MODEL.md`', 'Security, secret, permission, or evidence-safety work', 'Project security invariants and special checks.']);
975
975
  }
976
- if (spec.docs.refactorLog) {
977
- requiredReadingRows.push(['`docs/REFACTOR_LOG.md`', 'Refactor, migration, removal, or replacement work', 'Project-level refactor history.']);
978
- }
979
976
  if (spec.docs.roadmap) {
980
977
  requiredReadingRows.push(['`docs/ROADMAP.md`', 'Roadmap, milestone, release, or scope planning', 'Longer-term priorities and deferred work.']);
981
978
  }
@@ -1095,6 +1092,16 @@ Keep capsule docs current as work changes:
1095
1092
 
1096
1093
  Parallelize read-only discovery, \`rg\`/file inspection, independent validation commands, package or registry metadata inspection, read-only diagnostics, and draft preparation before writes. Serialize same-file writes, evidence append, Task Capsule doc writes, Task Board writes, Project State writes, Agent Handoff writes, before-hash execute operations, \`task finish --execute\`, \`task close --execute\`, and release artifact or publish operations.
1097
1094
 
1095
+ ## Status Token And Document Ownership Policy
1096
+
1097
+ Use distinct token families for persistent task state, close proof state, document registry state, and evidence outcomes. \`TaskStatus\` belongs to Task Capsule metadata, \`TASK.md\` Status/Status History, and command-owned \`docs/TASK_BOARD.md\` cells. Valid persistent task tokens are \`Draft\`, \`In Progress\`, \`Blocked\`, \`Done\`, \`Partial\`, \`Superseded\`, and \`Archived\`.
1098
+
1099
+ \`CloseState\` is derived from close evidence and \`task audit-close\`; do not write close proof values as \`TaskStatus\`, and do not persist \`CloseState\` in task-local \`HANDOFF.md\` close-source current-state tables. Canonical close-state tokens are \`not-closed\`, \`closed-valid\`, \`closed-stale\`, \`closed-invalid\`, and \`unknown\`. Compatibility diagnostics such as \`close-evidence-found-invalid\`, \`close-evidence-malformed\`, and \`closed-with-drift-warnings\` are close-state details, not task status.
1100
+
1101
+ \`DocStatus\` belongs only to the docs registry and uses \`canonical\`, \`active\`, \`reference\`, \`historical\`, \`superseded\`, and \`archived\`. Evidence outcomes are \`passed\`, \`failed\`, \`blocked\`, and \`unknown\`; preserve failed or blocked evidence and append newer corrective evidence instead of rewriting history.
1102
+
1103
+ Ownership boundaries follow the lifecycle command model. \`task finish --execute\` owns bounded status bookkeeping in \`TASK.md\` and command-owned \`docs/TASK_BOARD.md\` cells. \`task close --execute\` owns only close evidence append. Operators own close-source prose and shared state docs before close, then rerun ready/close/audit after any intentional close-source edit.
1104
+
1098
1105
  ## Standard Task Workflow Loop
1099
1106
 
1100
1107
  The authoritative command semantics live in \`docs/TASK_WORKFLOW_COMMANDS.md\`. For ordinary implementation capsules, use this loop:
@@ -1353,6 +1360,68 @@ The close model has three separate phases: validation proves readiness, close re
1353
1360
 
1354
1361
  \`task ready\` and \`task close\` include done-level Task Capsule validation. Use \`hadara harness validate --task T-XXXX --level done --json\` directly when debugging capsule format, status-history, acceptance, evidence, or handoff validation failures.
1355
1362
 
1363
+ ## Status Token And Ownership Policy
1364
+
1365
+ HADARA uses separate token families for persistent state, derived proof state, document registry state, and evidence outcomes. Do not collapse these families into a single Markdown \`Status\` field.
1366
+
1367
+ ### TaskStatus
1368
+
1369
+ \`TaskStatus\` is persistent task lifecycle state in \`TASK.md\` metadata, the \`## Status\` section, Status History rows, and the command-owned cells of \`docs/TASK_BOARD.md\`.
1370
+
1371
+ | Token | Meaning | Writer |
1372
+ |---|---|---|
1373
+ | \`Draft\` | Task capsule exists but implementation is not started or not yet ready for done-level validation. | \`task create\`, worker docs |
1374
+ | \`In Progress\` | Work is actively being performed. | Worker docs |
1375
+ | \`Blocked\` | Work cannot proceed without a recorded blocker. | Worker docs |
1376
+ | \`Done\` | Scoped work is implemented and ready for done-level validation/close. | \`task finish --execute\` |
1377
+ | \`Partial\` | Deliberate partial completion with remaining scope deferred or split. | Worker/coordinator docs |
1378
+ | \`Superseded\` | Task has been replaced by another task or line. | Worker/coordinator docs |
1379
+ | \`Archived\` | Task is no longer active state and is retained only for history. | Worker/coordinator docs |
1380
+
1381
+ Reserved non-TaskStatus strings include \`Closed\`, \`Ready\`, \`Approved\`, \`Complete\`, \`closed-valid\`, \`not-closed\`, and phrases such as \`Done pending lifecycle close\`. Use \`TaskStatus: Done\`; get close proof state from \`task status\`, \`task audit-close\`, proof status, or \`state verify\` read models.
1382
+
1383
+ ### CloseState
1384
+
1385
+ \`CloseState\` is derived proof state from close evidence and \`task audit-close\`; it is not written as persistent \`TaskStatus\` and should not be stored in task-local \`HANDOFF.md\` current-state tables.
1386
+
1387
+ | Canonical Token | Meaning |
1388
+ |---|---|
1389
+ | \`not-closed\` | No valid close proof has been recorded. |
1390
+ | \`closed-valid\` | Close evidence exists and audit reports current/fresh proof. |
1391
+ | \`closed-stale\` | Close evidence exists but source or validation hashes drifted after close. |
1392
+ | \`closed-invalid\` | Close-like evidence exists but audit reports invalid shape, failed result, or mismatch. |
1393
+ | \`unknown\` | The projection cannot determine close state. |
1394
+
1395
+ Current compatibility read models may expose more specific diagnostic values such as \`close-evidence-found-invalid\`, \`close-evidence-malformed\`, or \`closed-with-drift-warnings\`. Treat those as CloseState diagnostics, not TaskStatus values.
1396
+
1397
+ ### DocStatus
1398
+
1399
+ \`DocStatus\` is stored in the document registry only.
1400
+
1401
+ | Token | Meaning |
1402
+ |---|---|
1403
+ | \`canonical\` | Core scaffold/current-state document. |
1404
+ | \`active\` | Active working document or task-work document. |
1405
+ | \`reference\` | Conditional reference document. |
1406
+ | \`historical\` | Historical context, never default required reading. |
1407
+ | \`superseded\` | Replaced by another registered document. |
1408
+ | \`archived\` | Retained only as archive candidate/history. |
1409
+
1410
+ ### EvidenceOutcome
1411
+
1412
+ Evidence outcome tokens are \`passed\`, \`failed\`, \`blocked\`, and \`unknown\`. Failed or blocked evidence must remain visible; add newer evidence that explains the fix or residual risk instead of editing old records.
1413
+
1414
+ ### Write Ownership
1415
+
1416
+ | Surface | Ownership |
1417
+ |---|---|
1418
+ | \`TASK.md\` status metadata, \`## Status\`, and Status History | Command-owned for finish bookkeeping; worker-owned before finish. |
1419
+ | \`docs/TASK_BOARD.md\` ID/title/status/capsule cells | Command-owned by \`task finish\`; Notes and extra cells are mixed/human-owned. |
1420
+ | \`EVIDENCE.md\` and \`evidence.jsonl\` | Evidence writer-owned; do not hand-edit \`evidence.jsonl\`. |
1421
+ | \`HANDOFF.md\` managed current-state table | Managed/mixed; persist \`TaskStatus\` only. \`CloseState\` is derived by status/audit/proof/state read models and should not be written into close-source handoff tables. |
1422
+ | Shared state docs | Mixed/human-owned; update before close when they are close-source relevant. |
1423
+ | \`.hadara/docs-registry.json\` and \`docs/DOC_REGISTRY.md\` | Docs registry-owned; registry mutations should stay dry-run-first or explicitly scoped. |
1424
+
1356
1425
  Before close, finish all close-source edits: Task Capsule docs, acceptance/tests/handoff notes, evidence summaries, \`docs/TASK_BOARD.md\`, and tracked state docs such as \`docs/PROJECT_STATE.md\`, \`docs/AGENT_HANDOFF.md\`, and roadmap/slice docs when they apply. After \`task close --execute --json\`, changing those documents changes the close source hash and requires rerunning \`task ready\`, \`task close\`, and \`task audit-close\`. Do not paste volatile close evidence ids into close-source docs; prefer stable wording such as "close evidence appended; audit returned closed-valid".
1357
1426
 
1358
1427
  ## Documentation Timing and Write Coordination
@@ -1415,8 +1484,6 @@ function createAgentsDoc(spec) {
1415
1484
  requiredReadingRows.push([String(order++), '`docs/TEST_STRATEGY.md`', 'Validation planning or completion checks', 'Routine suites and special-case checks.']);
1416
1485
  if (spec.docs.securityModel)
1417
1486
  requiredReadingRows.push([String(order++), '`docs/SECURITY_MODEL.md`', 'Security, secret, permission, or evidence-safety work', 'Project security invariants.']);
1418
- if (spec.docs.refactorLog)
1419
- requiredReadingRows.push([String(order++), '`docs/REFACTOR_LOG.md`', 'Refactor, migration, removal, or replacement work', 'Project-level refactor history.']);
1420
1487
  if (spec.docs.roadmap)
1421
1488
  requiredReadingRows.push([String(order++), '`docs/ROADMAP.md`', 'Roadmap, milestone, or scope planning', 'Longer-term priorities and deferred work.']);
1422
1489
  requiredReadingRows.push([String(order++), 'Active `tasks/T-*/TASK.md`', 'Working a task', 'Task-specific goal, scope, and status.'], [String(order++), 'Active Task Capsule docs', 'Working a task', 'Decisions, plan, context, acceptance, files, tests, risks, handoff, and evidence.'], [String(order++), 'Project-specific registered docs', 'When listed in `docs/IMPLEMENTATION_SOP.md`', 'Specs, contracts, or roadmap files explicitly added by this project.']);
package/dist/cli/main.js CHANGED
@@ -173,6 +173,12 @@ async function main(args = process.argv.slice(2)) {
173
173
  return;
174
174
  break;
175
175
  }
176
+ case 'state': {
177
+ const { handleStateCommand } = await Promise.resolve().then(() => __importStar(require('./state')));
178
+ if (handleStateCommand({ args, projectRoot: paths.projectRoot, jsonOutput }))
179
+ return;
180
+ break;
181
+ }
176
182
  case 'dashboard': {
177
183
  const { handleDashboardCommand } = await Promise.resolve().then(() => __importStar(require('./dashboard')));
178
184
  if (handleDashboardCommand({ args, projectRoot: paths.projectRoot }))
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleStateCommand = handleStateCommand;
4
+ const state_projection_1 = require("../services/state-projection");
5
+ function handleStateCommand(input) {
6
+ if (input.args[0] !== 'state' || input.args[1] !== 'verify')
7
+ return false;
8
+ const report = (0, state_projection_1.createStateProjectionReport)(input.projectRoot);
9
+ if (input.jsonOutput) {
10
+ console.log(JSON.stringify(report, null, 2));
11
+ }
12
+ else {
13
+ console.log((0, state_projection_1.formatStateProjectionReport)(report));
14
+ }
15
+ return true;
16
+ }
@@ -16,7 +16,7 @@ function handleOpsCommand(input) {
16
16
  return true;
17
17
  }
18
18
  function printStatus(projectRoot, jsonOutput) {
19
- const report = (0, operations_status_service_1.createOpsStatusReport)(projectRoot);
19
+ const report = (0, operations_status_service_1.createOpsStatusReport)(projectRoot, { includeStateConsistency: true });
20
20
  if (jsonOutput) {
21
21
  console.log(JSON.stringify(report, null, 2));
22
22
  }
@@ -49,6 +49,7 @@ const release_gate_schema_json_1 = __importDefault(require("../schemas/release-g
49
49
  const release_publish_schema_json_1 = __importDefault(require("../schemas/release-publish.schema.json"));
50
50
  const runtime_version_schema_json_1 = __importDefault(require("../schemas/runtime-version.schema.json"));
51
51
  const smoke_evidence_summary_schema_json_1 = __importDefault(require("../schemas/smoke-evidence-summary.schema.json"));
52
+ const state_projection_schema_json_1 = __importDefault(require("../schemas/state-projection.schema.json"));
52
53
  const task_audit_close_schema_json_1 = __importDefault(require("../schemas/task-audit-close.schema.json"));
53
54
  const task_close_schema_json_1 = __importDefault(require("../schemas/task-close.schema.json"));
54
55
  const task_complete_flow_schema_json_1 = __importDefault(require("../schemas/task-complete-flow.schema.json"));
@@ -115,6 +116,7 @@ const registeredSchemas = {
115
116
  'hadara.releasePublish.v1': release_publish_schema_json_1.default,
116
117
  'hadara.runtime.version.v1': runtime_version_schema_json_1.default,
117
118
  'hadara.smokeEvidenceSummary.v1': smoke_evidence_summary_schema_json_1.default,
119
+ 'hadara.stateProjection.v1': state_projection_schema_json_1.default,
118
120
  'hadara.task.audit_close.v1': task_audit_close_schema_json_1.default,
119
121
  'hadara.task.close.v1': task_close_schema_json_1.default,
120
122
  'hadara.task.complete_flow.v1': task_complete_flow_schema_json_1.default,
@@ -25,6 +25,8 @@ const REQUIRED_TASK_FILES = [
25
25
  const EVIDENCE_KINDS = new Set(['test-log', 'command-log', 'diff-summary', 'screenshot', 'note']);
26
26
  const EVIDENCE_RESULTS = new Set(['passed', 'failed', 'blocked', 'unknown']);
27
27
  const EVIDENCE_VISIBILITIES = new Set(['public', 'private']);
28
+ const TASK_STATUS_TOKENS = new Set(['draft', 'in progress', 'blocked', 'done', 'partial', 'superseded', 'archived']);
29
+ const STALE_PENDING_CLOSE_PATTERN = /\b(?:done\s+pending\s+lifecycle\s+close|pending\s+lifecycle\s+close)\b/i;
28
30
  const DONE_SEMANTIC_EVIDENCE_CODES = new Set([
29
31
  'TASK_DONE_WITHOUT_SUBSTANTIVE_EVIDENCE',
30
32
  'TASK_DONE_WITH_FAILED_EVIDENCE',
@@ -338,6 +340,8 @@ function validateDoneLevel(projectRoot, task, issues, checkedFiles) {
338
340
  validateEvidenceIndexHasRecords(projectRoot, task, issues);
339
341
  validateEvidenceSemanticGates(projectRoot, task, issues);
340
342
  validateHandoffDone(projectRoot, task, issues);
343
+ validateHandoffCurrentStateTokens(projectRoot, task, issues);
344
+ validatePlanStatusDrift(projectRoot, task, issues);
341
345
  validateTaskBoardDone(projectRoot, task, issues, checkedFiles);
342
346
  }
343
347
  function validateTaskMetadataComplete(projectRoot, task, issues) {
@@ -645,6 +649,112 @@ function validateHandoffDone(projectRoot, task, issues) {
645
649
  });
646
650
  }
647
651
  }
652
+ function validateHandoffCurrentStateTokens(projectRoot, task, issues) {
653
+ const handoffPath = node_path_1.default.join(task.dir, 'HANDOFF.md');
654
+ if (!node_fs_1.default.existsSync(handoffPath))
655
+ return;
656
+ const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, handoffPath));
657
+ const currentState = readSectionBody(handoffPath, '## Current State');
658
+ const fields = handoffCurrentStateFields(currentState);
659
+ const taskStatus = fields.get('taskstatus') ?? fields.get('task status');
660
+ const legacyStatus = fields.get('status');
661
+ const closeState = fields.get('closestate') ?? fields.get('close state');
662
+ if (taskStatus && !isTaskStatusToken(taskStatus)) {
663
+ issues.push(handoffStatusIssue(relativePath, `HANDOFF.md TaskStatus uses non-canonical value "${taskStatus}".`));
664
+ }
665
+ if (legacyStatus) {
666
+ if (STALE_PENDING_CLOSE_PATTERN.test(legacyStatus)) {
667
+ issues.push(handoffStatusIssue(relativePath, 'HANDOFF.md legacy Status mixes lifecycle state with pending close proof wording.'));
668
+ }
669
+ else if (!isTaskStatusToken(legacyStatus)) {
670
+ issues.push(handoffStatusIssue(relativePath, `HANDOFF.md legacy Status uses non-canonical value "${legacyStatus}".`));
671
+ }
672
+ }
673
+ if (STALE_PENDING_CLOSE_PATTERN.test(currentState) && (!legacyStatus || !STALE_PENDING_CLOSE_PATTERN.test(legacyStatus))) {
674
+ issues.push(handoffStatusIssue(relativePath, 'HANDOFF.md Current State contains stale pending lifecycle close wording.'));
675
+ }
676
+ if (closeState) {
677
+ issues.push({
678
+ severity: 'error',
679
+ code: 'TASK_HANDOFF_CLOSE_STATE_PERSISTED',
680
+ message: `HANDOFF.md persists derived CloseState value "${closeState}".`,
681
+ path: relativePath,
682
+ heading: 'Current State',
683
+ fixHint: 'Remove the CloseState row from task-local HANDOFF.md; use task status, audit-close, proof status, or state verify read models for derived close state.',
684
+ example: '| TaskStatus | Done |',
685
+ remediationHint: {
686
+ path: relativePath,
687
+ heading: 'Current State',
688
+ requiredChange: 'Remove persistent CloseState from this close-source handoff table.',
689
+ example: '| TaskStatus | Done |',
690
+ blocking: true
691
+ }
692
+ });
693
+ }
694
+ }
695
+ function handoffCurrentStateFields(currentState) {
696
+ const fields = new Map();
697
+ for (const cells of (0, markdown_table_1.parseMarkdownRows)(currentState)) {
698
+ if (cells.length < 2 || /^field$/i.test(cells[0] ?? ''))
699
+ continue;
700
+ fields.set(normalizeFieldName(cells[0] ?? ''), cells[1]?.trim() ?? '');
701
+ }
702
+ return fields;
703
+ }
704
+ function handoffStatusIssue(relativePath, message) {
705
+ return {
706
+ severity: 'error',
707
+ code: 'TASK_HANDOFF_STATUS_DRIFT',
708
+ message,
709
+ path: relativePath,
710
+ heading: 'Current State',
711
+ fixHint: 'Use `TaskStatus` for lifecycle state only; close proof state is derived from audit-close/proof/status read models.',
712
+ example: '| TaskStatus | Done |',
713
+ remediationHint: {
714
+ path: relativePath,
715
+ heading: 'Current State',
716
+ requiredChange: 'Replace ambiguous Status wording with canonical TaskStatus and remove close-proof wording from HANDOFF.md.',
717
+ example: '| TaskStatus | Done |',
718
+ blocking: true
719
+ }
720
+ };
721
+ }
722
+ function validatePlanStatusDrift(projectRoot, task, issues) {
723
+ const planPath = node_path_1.default.join(task.dir, 'PLAN.md');
724
+ if (!node_fs_1.default.existsSync(planPath))
725
+ return;
726
+ const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, planPath));
727
+ const rows = (0, markdown_table_1.parseMarkdownRows)(node_fs_1.default.readFileSync(planPath, 'utf8'));
728
+ const inProgressRows = rows.filter((cells) => {
729
+ if (/^step$/i.test(cells[0] ?? ''))
730
+ return false;
731
+ return (cells[2] ?? '').trim().toLowerCase() === 'in progress';
732
+ });
733
+ if (inProgressRows.length === 0)
734
+ return;
735
+ issues.push({
736
+ severity: 'error',
737
+ code: 'TASK_PLAN_STATUS_DRIFT',
738
+ message: `PLAN.md has ${inProgressRows.length} row(s) still marked In Progress while the task is Done.`,
739
+ path: relativePath,
740
+ heading: 'Plan',
741
+ fixHint: 'Before closing a Done task, mark completed plan rows Done or split/defer unfinished work explicitly instead of leaving rows In Progress.',
742
+ example: '| 3 | Commit the pre-publish preparation. | Done | `command:T-XXXX:check` |',
743
+ remediationHint: {
744
+ path: relativePath,
745
+ heading: 'Plan',
746
+ requiredChange: 'Update PLAN.md rows that are no longer active from In Progress to Done, Blocked, Partial, or a task-specific final status with evidence.',
747
+ example: '| 3 | Commit the pre-publish preparation. | Done | `command:T-XXXX:check` |',
748
+ blocking: true
749
+ }
750
+ });
751
+ }
752
+ function isTaskStatusToken(value) {
753
+ return TASK_STATUS_TOKENS.has(value.trim().toLowerCase());
754
+ }
755
+ function normalizeFieldName(value) {
756
+ return value.trim().toLowerCase().replace(/\s+/g, ' ');
757
+ }
648
758
  function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
649
759
  const taskBoardPath = node_path_1.default.join(projectRoot, 'docs', 'TASK_BOARD.md');
650
760
  const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, taskBoardPath));
@@ -274,6 +274,13 @@
274
274
  "owner": "runtime/version",
275
275
  "notes": "Documents read-only CLI origin, package, git, Node, and build freshness reports from hadara version --verbose."
276
276
  },
277
+ {
278
+ "id": "hadara.stateProjection.v1",
279
+ "path": "src/schemas/state-projection.schema.json",
280
+ "status": "fixture",
281
+ "owner": "services/state-projection",
282
+ "notes": "Documents the Phase 8.4 read-only state consistency projection over Task Board, Task Capsules, handoff, close evidence, docs registry, and release readiness state."
283
+ },
277
284
  {
278
285
  "id": "hadara.releaseGate.v1",
279
286
  "path": "src/schemas/release-gate.schema.json",
@@ -0,0 +1,115 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "hadara.stateProjection.v1",
4
+ "x-hadara-schema-id": "hadara.stateProjection.v1",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["schemaVersion", "command", "ok", "generatedAt", "projectRoot", "summary", "sources", "tasks", "issues"],
8
+ "properties": {
9
+ "schemaVersion": {
10
+ "const": "hadara.stateProjection.v1"
11
+ },
12
+ "command": {
13
+ "const": "state.projection"
14
+ },
15
+ "ok": {
16
+ "type": "boolean"
17
+ },
18
+ "generatedAt": {
19
+ "type": "string"
20
+ },
21
+ "projectRoot": {
22
+ "type": "string"
23
+ },
24
+ "summary": {
25
+ "type": "object",
26
+ "required": ["consistent", "issueCounts", "latestDoneTaskId", "activeTaskIds", "checkedTasks"],
27
+ "additionalProperties": true,
28
+ "properties": {
29
+ "consistent": {
30
+ "type": "boolean"
31
+ },
32
+ "issueCounts": {
33
+ "type": "object"
34
+ },
35
+ "latestDoneTaskId": {
36
+ "type": ["string", "null"]
37
+ },
38
+ "activeTaskIds": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "string"
42
+ }
43
+ },
44
+ "checkedTasks": {
45
+ "type": "number"
46
+ }
47
+ }
48
+ },
49
+ "sources": {
50
+ "type": "object"
51
+ },
52
+ "tasks": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "required": ["id", "title", "capsule", "task", "taskBoard", "handoff", "plan", "closeProof"],
57
+ "additionalProperties": true,
58
+ "properties": {
59
+ "id": {
60
+ "type": "string"
61
+ },
62
+ "title": {
63
+ "type": "string"
64
+ },
65
+ "capsule": {
66
+ "type": "string"
67
+ },
68
+ "task": {
69
+ "type": "object"
70
+ },
71
+ "taskBoard": {
72
+ "type": "object"
73
+ },
74
+ "handoff": {
75
+ "type": "object"
76
+ },
77
+ "plan": {
78
+ "type": "object"
79
+ },
80
+ "closeProof": {
81
+ "type": "object"
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "issues": {
87
+ "type": "array",
88
+ "items": {
89
+ "type": "object",
90
+ "required": ["severity", "code", "message", "fixHint"],
91
+ "additionalProperties": true,
92
+ "properties": {
93
+ "severity": {
94
+ "enum": ["error", "warning", "info"]
95
+ },
96
+ "code": {
97
+ "type": "string"
98
+ },
99
+ "message": {
100
+ "type": "string"
101
+ },
102
+ "path": {
103
+ "type": "string"
104
+ },
105
+ "taskId": {
106
+ "type": "string"
107
+ },
108
+ "fixHint": {
109
+ "type": "string"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
@@ -600,7 +600,7 @@ exports.HADARA_COMMAND_REGISTRY = [
600
600
  {
601
601
  id: 'ci.gate',
602
602
  command: 'hadara ci gate [--mode advisory|strict] [--task <task-id>] [--allow-empty] [--json]',
603
- summary: 'Evaluate CI-style task proof gates.',
603
+ summary: 'Evaluate CI-style task proof gates with advisory state consistency signals.',
604
604
  canonical: true,
605
605
  appearsInDefaultHelp: false,
606
606
  family: 'proof-diagnostics',
@@ -614,7 +614,28 @@ exports.HADARA_COMMAND_REGISTRY = [
614
614
  status: 'stable',
615
615
  docs: ['docs/IMPLEMENTATION_SOP.md'],
616
616
  examples: [example('Run strict CI gate', 'hadara ci gate --mode strict --task T-0001 --json', 'In automated validation paths.')],
617
- related: ['task.ready', 'proof.status'],
617
+ related: ['task.ready', 'proof.status', 'state.verify'],
618
+ conflictsWith: []
619
+ },
620
+ {
621
+ id: 'state.verify',
622
+ command: 'hadara state verify [--json]',
623
+ summary: 'Read the state consistency projection and concise drift issues.',
624
+ canonical: true,
625
+ appearsInDefaultHelp: false,
626
+ family: 'proof-diagnostics',
627
+ scope: 'project',
628
+ lifecycleStage: 'ready',
629
+ requiredness: 'diagnostic',
630
+ writeBoundary: 'read-only',
631
+ readOnly: true,
632
+ risk: 'low',
633
+ actor: 'agent-worker',
634
+ status: 'stable',
635
+ schemaVersion: 'hadara.stateProjection.v1',
636
+ docs: ['docs/COMMAND_SURFACE.md', 'docs/SCHEMAS.md'],
637
+ examples: [example('Verify state drift', 'hadara state verify --json', 'Before close or when shared-doc state looks inconsistent.')],
638
+ related: ['status', 'protocol.doctor', 'ci.gate'],
618
639
  conflictsWith: []
619
640
  },
620
641
  commandEntry({
@@ -677,7 +698,7 @@ exports.HADARA_COMMAND_REGISTRY = [
677
698
  schemaVersion: 'hadara.protocol.consistency.v1',
678
699
  docs: ['docs/IMPLEMENTATION_SOP.md'],
679
700
  examples: [example('Run protocol doctor', 'hadara protocol doctor --scope all --json', 'When project protocol files may be inconsistent.')],
680
- related: ['protocol.remediate', 'doctor'],
701
+ related: ['protocol.remediate', 'doctor', 'state.verify'],
681
702
  conflictsWith: []
682
703
  }),
683
704
  commandEntry({
@@ -1185,7 +1206,7 @@ exports.HADARA_COMMAND_REGISTRY = [
1185
1206
  schemaVersion: 'hadara.ops.status.v1',
1186
1207
  docs: ['docs/PROJECT_STATE.md'],
1187
1208
  examples: [example('Read status', 'hadara status --json', 'When checking project health and active task signals.')],
1188
- related: ['ops.status', 'doctor'],
1209
+ related: ['ops.status', 'doctor', 'state.verify'],
1189
1210
  conflictsWith: []
1190
1211
  }),
1191
1212
  commandEntry({