hadara 0.3.0 → 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 +27 -27
- package/dist/cli/init.js +75 -8
- package/dist/cli/main.js +6 -0
- package/dist/cli/state.js +16 -0
- package/dist/cli/status.js +1 -1
- package/dist/core/schema.js +2 -0
- package/dist/harness/validate.js +110 -0
- package/dist/schemas/schema-index.json +7 -0
- package/dist/schemas/state-projection.schema.json +115 -0
- package/dist/services/capability-registry.js +25 -4
- package/dist/services/ci-gate.js +23 -0
- package/dist/services/docs-registry.js +3 -1
- package/dist/services/operational-debt.js +2 -2
- package/dist/services/operations-status-service.js +11 -0
- package/dist/services/protocol-consistency.js +3 -0
- package/dist/services/protocol-profile.js +1 -1
- package/dist/services/release-publish.js +3 -2
- package/dist/services/state-projection.js +406 -0
- package/dist/task/task-capsule.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
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.
|
|
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>
|
|
@@ -27,26 +27,22 @@ Current stable npm release:
|
|
|
27
27
|
hadara@0.3.0
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Current release candidate:
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
|
-
hadara@0.3.
|
|
33
|
+
hadara@0.3.1-rc.1
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
The 0.3.
|
|
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
|
-
Phase
|
|
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.
|
|
39
39
|
|
|
40
40
|
| Surface | Status |
|
|
41
41
|
|---|---|
|
|
42
|
-
|
|
|
43
|
-
| `hadara@0.
|
|
44
|
-
| `hadara@0.
|
|
45
|
-
|
|
|
46
|
-
| `hadara@0.3.0-rc.0` | Previous published npm RC; package metadata lacks the intended discovery fields. |
|
|
47
|
-
| `hadara@0.3.0-rc.1` | Previous published npm RC; T-0301 publish evidence verified `npm view` returned `0.3.0-rc.1`. |
|
|
48
|
-
| `hadara@0.3.0-rc.2` | Previous published npm RC; T-0310 publish evidence verified `npm view` returned `0.3.0-rc.2`. |
|
|
49
|
-
| `hadara@0.3.0` | Current stable npm release. |
|
|
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). |
|
|
50
46
|
| GitHub Release | Secondary target, approval-gated. |
|
|
51
47
|
| Docker image | Deferred. |
|
|
52
48
|
| PyPI/Python package | `hadara==0.2.0rc1` published preview bridge. |
|
|
@@ -73,6 +69,23 @@ npx hadara@0.3.0 help
|
|
|
73
69
|
npx hadara@0.3.0 doctor --json
|
|
74
70
|
```
|
|
75
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
|
+
|
|
76
89
|
## What HADARA Gives You
|
|
77
90
|
|
|
78
91
|
| Capability | Purpose |
|
|
@@ -219,7 +232,7 @@ Dashboard, TUI, Hermes, MCP, installer, package, release, and run commands stay
|
|
|
219
232
|
|
|
220
233
|
## Safety Boundaries
|
|
221
234
|
|
|
222
|
-
HADARA 0.3.
|
|
235
|
+
HADARA 0.3.1-rc.1 is not:
|
|
223
236
|
|
|
224
237
|
- a full agent runtime;
|
|
225
238
|
- Rack/enterprise behavior;
|
|
@@ -292,19 +305,6 @@ node dist/cli/main.js doctor --json
|
|
|
292
305
|
npm run check
|
|
293
306
|
```
|
|
294
307
|
|
|
295
|
-
For HADARA-dev itself, Docker is the preferred validation path because host `node_modules` on mounted workspaces can be unreliable:
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
npm run dev:docker-check
|
|
299
|
-
npm run dev:docker-sync-build
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
Focused validation:
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
npm run test:focused -- tests/unit/release-dry-run.test.ts
|
|
306
|
-
```
|
|
307
|
-
|
|
308
308
|
Optional integrations are not generated by `hadara init` and are not part of the default scaffold:
|
|
309
309
|
|
|
310
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/
|
|
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/
|
|
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/
|
|
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
|
+
}
|
package/dist/cli/status.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/schema.js
CHANGED
|
@@ -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,
|
package/dist/harness/validate.js
CHANGED
|
@@ -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({
|
package/dist/services/ci-gate.js
CHANGED
|
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const proof_status_1 = require("./proof-status");
|
|
10
10
|
const evidence_lint_1 = require("./evidence-lint");
|
|
11
11
|
const protocol_consistency_1 = require("./protocol-consistency");
|
|
12
|
+
const state_projection_1 = require("./state-projection");
|
|
12
13
|
const task_capsule_1 = require("../task/task-capsule");
|
|
13
14
|
function createCiGateReport(projectRoot, mode, options = {}) {
|
|
14
15
|
const allowEmpty = options.allowEmpty ?? false;
|
|
@@ -29,6 +30,27 @@ function createCiGateReport(projectRoot, mode, options = {}) {
|
|
|
29
30
|
const target = issue.severity === 'error' ? blockers : warnings;
|
|
30
31
|
target.push({ severity: issue.severity, source: 'protocol', code: issue.code, message: issue.message, path: issue.path });
|
|
31
32
|
}
|
|
33
|
+
const stateProjection = (0, state_projection_1.createStateProjectionReport)(projectRoot);
|
|
34
|
+
const stateConsistency = (0, state_projection_1.toStateProjectionAdvisory)(stateProjection, 10);
|
|
35
|
+
checks.push({
|
|
36
|
+
id: 'state:consistency',
|
|
37
|
+
source: 'state',
|
|
38
|
+
ok: true,
|
|
39
|
+
summary: stateConsistency.consistent
|
|
40
|
+
? 'State consistency projection found no drift.'
|
|
41
|
+
: `State consistency projection is advisory: warnings ${stateConsistency.issueCounts.warning}, errors ${stateConsistency.issueCounts.error}.`
|
|
42
|
+
});
|
|
43
|
+
for (const issue of stateConsistency.issues) {
|
|
44
|
+
warnings.push({
|
|
45
|
+
severity: issue.severity,
|
|
46
|
+
source: 'state',
|
|
47
|
+
code: issue.code,
|
|
48
|
+
message: issue.message,
|
|
49
|
+
taskId: issue.taskId,
|
|
50
|
+
path: issue.path,
|
|
51
|
+
fixHint: issue.fixHint
|
|
52
|
+
});
|
|
53
|
+
}
|
|
32
54
|
for (const task of tasks) {
|
|
33
55
|
const evidence = (0, evidence_lint_1.createEvidenceLintReport)(projectRoot, task.id);
|
|
34
56
|
checks.push({
|
|
@@ -63,6 +85,7 @@ function createCiGateReport(projectRoot, mode, options = {}) {
|
|
|
63
85
|
ok: mode === 'advisory' ? true : blockers.length === 0,
|
|
64
86
|
mode,
|
|
65
87
|
scope: { ...(options.taskId ? { taskId: options.taskId } : {}), taskCount: tasks.length, allowEmpty },
|
|
88
|
+
stateConsistency,
|
|
66
89
|
checks,
|
|
67
90
|
blockers,
|
|
68
91
|
warnings
|
|
@@ -14,6 +14,7 @@ exports.registryJson = registryJson;
|
|
|
14
14
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
16
|
const managed_sections_1 = require("./managed-sections");
|
|
17
|
+
const markdown_table_1 = require("./markdown-table");
|
|
17
18
|
exports.DOCS_REGISTRY_PATH = '.hadara/docs-registry.json';
|
|
18
19
|
const VALID_STATUSES = ['canonical', 'active', 'reference', 'historical', 'superseded', 'archived'];
|
|
19
20
|
const VALID_READ_WHEN = ['session-start', 'task-start', 'task-close', 'release-work', 'docs-work', 'debugging', 'integration-work', 'only-when-linked', 'never-default'];
|
|
@@ -294,7 +295,8 @@ function parseRequiredReading(projectRoot) {
|
|
|
294
295
|
const found = new Set();
|
|
295
296
|
for (const file of files) {
|
|
296
297
|
const text = node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, file)) ? node_fs_1.default.readFileSync(node_path_1.default.join(projectRoot, file), 'utf8') : '';
|
|
297
|
-
|
|
298
|
+
const requiredReading = (0, markdown_table_1.readMarkdownSection)(text, '## Required Reading');
|
|
299
|
+
for (const match of requiredReading.matchAll(/`([^`]+\.(?:md|MD))`/g)) {
|
|
298
300
|
const value = normalizePath(match[1]);
|
|
299
301
|
if (value !== 'AGENTS.md' && !value.startsWith('docs/') && value !== '.hadara/context/HADARA_CONTEXT.md')
|
|
300
302
|
continue;
|
|
@@ -335,7 +335,7 @@ function checkPackageMetadataReadiness(packageJson, licenseText, testStrategy, r
|
|
|
335
335
|
packageJson?.private === true &&
|
|
336
336
|
bin.hadara === './dist/cli/main.js';
|
|
337
337
|
const publishablePackageMetadataOk = packageJson?.name === 'hadara' &&
|
|
338
|
-
|
|
338
|
+
/^0\.\d+\.\d+(?:-rc\.\d+)?$/.test(String(packageJson?.version ?? '')) &&
|
|
339
339
|
packageJson?.private === false &&
|
|
340
340
|
packageJson?.license === 'MIT' &&
|
|
341
341
|
bin.hadara === './dist/cli/main.js' &&
|
|
@@ -351,7 +351,7 @@ function checkPackageMetadataReadiness(packageJson, licenseText, testStrategy, r
|
|
|
351
351
|
'Current binary remains `bin.hadara` at `./dist/cli/main.js`',
|
|
352
352
|
'Current `files` whitelist is `dist/`, `README.md`, `LICENSE`, and `package.json`',
|
|
353
353
|
'Bootstrap metadata mode: version `0.0.0-bootstrap`, `private: true`, no package publishability',
|
|
354
|
-
'Release-candidate metadata mode: version `0.x.
|
|
354
|
+
'Release-candidate metadata mode: version `0.x.y-rc.N`, `private: false`, `files` whitelist present, `LICENSE` present, package smoke evidence present',
|
|
355
355
|
'Scoped fallback decision: do not silently switch names',
|
|
356
356
|
'Version policy:',
|
|
357
357
|
'T-0142 transitions `private` to false only after the package files whitelist, root README, license decision, and package-smoke evidence gates exist',
|
|
@@ -12,6 +12,7 @@ const handoff_summary_parser_1 = require("./handoff-summary-parser");
|
|
|
12
12
|
const markdown_table_1 = require("./markdown-table");
|
|
13
13
|
const operational_debt_1 = require("./operational-debt");
|
|
14
14
|
const project_read_model_1 = require("./project-read-model");
|
|
15
|
+
const state_projection_1 = require("./state-projection");
|
|
15
16
|
const task_capsule_1 = require("../task/task-capsule");
|
|
16
17
|
const EMPTY_DEBT_AGGREGATE = {
|
|
17
18
|
total: 0,
|
|
@@ -35,6 +36,9 @@ function createOpsStatusReport(projectRoot, options = {}) {
|
|
|
35
36
|
const validation = (0, handoff_summary_parser_1.extractValidationBaselineSummary)(sources.handoff.content, sources.validationHistory.content);
|
|
36
37
|
const activeRun = (0, active_run_state_1.safeCreateActiveRunProjection)(projectRoot);
|
|
37
38
|
const debtAggregate = includeDebt ? (0, operational_debt_1.createOperationalDebtReport)(projectRoot).aggregate : EMPTY_DEBT_AGGREGATE;
|
|
39
|
+
const stateConsistency = options.includeStateConsistency
|
|
40
|
+
? (0, state_projection_1.toStateProjectionAdvisory)((0, state_projection_1.createStateProjectionReport)(projectRoot), options.stateIssueLimit ?? 10)
|
|
41
|
+
: undefined;
|
|
38
42
|
const issues = [...collectIssues(sources, validation), ...activeRun.issues];
|
|
39
43
|
return {
|
|
40
44
|
schemaVersion: 'hadara.ops.status.v1',
|
|
@@ -56,6 +60,7 @@ function createOpsStatusReport(projectRoot, options = {}) {
|
|
|
56
60
|
validation,
|
|
57
61
|
activeRun,
|
|
58
62
|
debt: debtAggregate,
|
|
63
|
+
...(stateConsistency ? { stateConsistency } : {}),
|
|
59
64
|
mcp: {
|
|
60
65
|
defaultMode: 'read-only',
|
|
61
66
|
evidenceAttach: {
|
|
@@ -78,6 +83,12 @@ function formatOpsStatusReport(report) {
|
|
|
78
83
|
`branch: ${report.project.branch}`,
|
|
79
84
|
`tasks: ${counts}`,
|
|
80
85
|
`debt: open ${report.debt.open}, highOpen ${report.debt.highOpen}`,
|
|
86
|
+
...(report.stateConsistency
|
|
87
|
+
? [
|
|
88
|
+
`stateConsistency: ${report.stateConsistency.consistent ? 'consistent' : 'drift'} ` +
|
|
89
|
+
`(errors ${report.stateConsistency.issueCounts.error}, warnings ${report.stateConsistency.issueCounts.warning}, info ${report.stateConsistency.issueCounts.info})`
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
81
92
|
`lastCompleted: ${report.tasks.lastCompleted.join(', ') || 'none'}`,
|
|
82
93
|
`nextRecommended: ${report.tasks.nextRecommended ?? 'none'}`
|
|
83
94
|
].join('\n');
|
|
@@ -12,6 +12,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
12
12
|
const protocol_profile_1 = require("./protocol-profile");
|
|
13
13
|
const evidence_lint_1 = require("./evidence-lint");
|
|
14
14
|
const markdown_table_1 = require("./markdown-table");
|
|
15
|
+
const state_projection_1 = require("./state-projection");
|
|
15
16
|
const task_capsule_1 = require("../task/task-capsule");
|
|
16
17
|
const REQUIRED_TASK_FILES = Object.keys(task_capsule_1.TASK_FILES);
|
|
17
18
|
const DONE_STATUSES = new Set(['done']);
|
|
@@ -126,6 +127,7 @@ function createAllProtocolConsistencyReport(projectRoot, now = new Date()) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
const counts = countIssues(issues);
|
|
130
|
+
const stateConsistency = (0, state_projection_1.toStateProjectionAdvisory)((0, state_projection_1.createStateProjectionReport)(projectRoot, now), 10);
|
|
129
131
|
return {
|
|
130
132
|
schemaVersion: 'hadara.protocol.consistency.v1',
|
|
131
133
|
command: 'protocol.doctor',
|
|
@@ -141,6 +143,7 @@ function createAllProtocolConsistencyReport(projectRoot, now = new Date()) {
|
|
|
141
143
|
profile: profileReport.summary.profile,
|
|
142
144
|
issueCounts: counts
|
|
143
145
|
},
|
|
146
|
+
stateConsistency,
|
|
144
147
|
issues,
|
|
145
148
|
remediations
|
|
146
149
|
};
|
|
@@ -217,7 +217,7 @@ function requiredDocsForProfile(profile) {
|
|
|
217
217
|
return CORE_PROJECT_DOCS;
|
|
218
218
|
}
|
|
219
219
|
function requiredReadingDocsForProfile(profile) {
|
|
220
|
-
return requiredDocsForProfile(profile).filter((relativePath) => relativePath !== 'AGENTS.md');
|
|
220
|
+
return requiredDocsForProfile(profile).filter((relativePath) => relativePath !== 'AGENTS.md' && relativePath !== 'docs/REFACTOR_LOG.md');
|
|
221
221
|
}
|
|
222
222
|
function readProfileMetadata(projectRoot) {
|
|
223
223
|
return {
|
|
@@ -10,11 +10,12 @@ const schema_1 = require("../core/schema");
|
|
|
10
10
|
const audit_1 = require("../core/audit");
|
|
11
11
|
const release_dry_run_1 = require("./release-dry-run");
|
|
12
12
|
const CONFIRMATION_PHRASE = 'publish-deploy';
|
|
13
|
+
const NPM_PUBLISHABLE_VERSION_PATTERN = /^0\.\d+\.\d+(?:-rc\.\d+)?$/;
|
|
13
14
|
function createReleasePublishReport(options) {
|
|
14
15
|
const mode = options.mode ?? 'dry-run';
|
|
15
16
|
const env = options.env ?? process.env;
|
|
16
17
|
const metadata = readPackageMetadata(options.projectRoot);
|
|
17
|
-
const metadataPublishable = !metadata.private &&
|
|
18
|
+
const metadataPublishable = !metadata.private && NPM_PUBLISHABLE_VERSION_PATTERN.test(metadata.packageVersion);
|
|
18
19
|
const dryRun = (0, release_dry_run_1.createReleaseDryRunReport)(options.projectRoot);
|
|
19
20
|
const approval = {
|
|
20
21
|
required: true,
|
|
@@ -35,7 +36,7 @@ function createReleasePublishReport(options) {
|
|
|
35
36
|
status: metadataPublishable ? 'passed' : 'error',
|
|
36
37
|
summary: metadataPublishable
|
|
37
38
|
? 'Package metadata is in publishable version mode.'
|
|
38
|
-
: 'Package metadata must be 0.x.
|
|
39
|
+
: 'Package metadata must be 0.x.y-rc.N or 0.x.y with private false before publish/deploy readiness can pass.'
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
42
|
code: 'APPROVAL_RECORD',
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createStateProjectionReport = createStateProjectionReport;
|
|
7
|
+
exports.toStateProjectionAdvisory = toStateProjectionAdvisory;
|
|
8
|
+
exports.formatStateProjectionReport = formatStateProjectionReport;
|
|
9
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const evidence_1 = require("../evidence/evidence");
|
|
13
|
+
const task_close_1 = require("../task/task-close");
|
|
14
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
15
|
+
const evidence_list_1 = require("./evidence-list");
|
|
16
|
+
const markdown_table_1 = require("./markdown-table");
|
|
17
|
+
const TASK_STATUS_TOKENS = new Set(['Draft', 'In Progress', 'Blocked', 'Done', 'Partial', 'Superseded', 'Archived']);
|
|
18
|
+
const CLOSE_STATE_TOKENS = new Set(['not-closed', 'closed-valid', 'closed-stale', 'closed-invalid', 'unknown']);
|
|
19
|
+
function createStateProjectionReport(projectRoot, now = new Date()) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
const sourceTexts = readSources(projectRoot, issues);
|
|
22
|
+
const taskBoardRows = parseTaskBoardRows(sourceTexts.taskBoard.content);
|
|
23
|
+
const tasks = (0, task_capsule_1.listTaskCapsules)(projectRoot);
|
|
24
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
25
|
+
const taskIds = new Set([...tasks.map((task) => task.id), ...taskBoardRows.map((row) => row.id)]);
|
|
26
|
+
const basicTaskStates = [...taskIds].sort().map((taskId) => ({
|
|
27
|
+
id: taskId,
|
|
28
|
+
taskStatus: taskById.get(taskId) ? readTaskStatus(node_path_1.default.join(taskById.get(taskId).dir, 'TASK.md')) : null,
|
|
29
|
+
taskBoardStatus: taskBoardRows.find((row) => row.id === taskId)?.status ?? null
|
|
30
|
+
}));
|
|
31
|
+
const latestDoneTaskId = latestTaskId(basicTaskStates.filter((task) => isDone(task.taskStatus) || isDone(task.taskBoardStatus)).map((task) => task.id));
|
|
32
|
+
const activeTaskIds = basicTaskStates.filter((task) => isActive(task.taskBoardStatus)).map((task) => task.id);
|
|
33
|
+
const deepCheckTaskIds = new Set([latestDoneTaskId, ...activeTaskIds].filter((value) => Boolean(value)));
|
|
34
|
+
const projectedTasks = [...taskIds]
|
|
35
|
+
.sort()
|
|
36
|
+
.map((taskId) => buildTaskProjection(projectRoot, taskId, taskById.get(taskId), taskBoardRows.find((row) => row.id === taskId), deepCheckTaskIds.has(taskId), issues));
|
|
37
|
+
checkLatestCloseProof(projectedTasks, latestDoneTaskId, issues);
|
|
38
|
+
const projectState = extractProjectState(sourceTexts.projectState);
|
|
39
|
+
const agentHandoff = extractAgentHandoff(sourceTexts.agentHandoff);
|
|
40
|
+
const developmentSlices = extractDevelopmentSlices(sourceTexts.developmentSlices);
|
|
41
|
+
const docsRegistry = extractDocsRegistry(projectRoot, issues);
|
|
42
|
+
const releaseReadiness = readSource(projectRoot, 'docs/RELEASE_READINESS.md');
|
|
43
|
+
if (!releaseReadiness.exists) {
|
|
44
|
+
issues.push(warning('STATE_RELEASE_READINESS_MISSING', releaseReadiness.path, 'docs/RELEASE_READINESS.md is missing.', 'Create or register release readiness docs before release work depends on this projection.'));
|
|
45
|
+
}
|
|
46
|
+
compareLatestTask('STATE_PROJECT_STATE_LATEST_MISMATCH', 'docs/PROJECT_STATE.md', projectState.latestCompletedTaskId, latestDoneTaskId, issues);
|
|
47
|
+
compareLatestTask('STATE_HANDOFF_LATEST_MISMATCH', 'docs/AGENT_HANDOFF.md', agentHandoff.latestCompletedTaskId, latestDoneTaskId, issues);
|
|
48
|
+
compareLatestTask('STATE_DEVELOPMENT_SLICES_LATEST_MISMATCH', 'docs/DEVELOPMENT_SLICES.md', developmentSlices.latestDoneTaskId, latestDoneTaskId, issues);
|
|
49
|
+
compareActiveTasks(projectState.activeTaskId, agentHandoff.activeTaskId, activeTaskIds, issues);
|
|
50
|
+
const counts = countIssues(issues);
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: 'hadara.stateProjection.v1',
|
|
53
|
+
command: 'state.projection',
|
|
54
|
+
ok: true,
|
|
55
|
+
generatedAt: now.toISOString(),
|
|
56
|
+
projectRoot,
|
|
57
|
+
summary: {
|
|
58
|
+
consistent: counts.error === 0 && counts.warning === 0,
|
|
59
|
+
issueCounts: counts,
|
|
60
|
+
latestDoneTaskId,
|
|
61
|
+
activeTaskIds,
|
|
62
|
+
checkedTasks: projectedTasks.length
|
|
63
|
+
},
|
|
64
|
+
sources: {
|
|
65
|
+
projectState: {
|
|
66
|
+
path: sourceTexts.projectState.path,
|
|
67
|
+
exists: sourceTexts.projectState.exists,
|
|
68
|
+
latestCompletedTaskId: projectState.latestCompletedTaskId,
|
|
69
|
+
activeTaskId: projectState.activeTaskId
|
|
70
|
+
},
|
|
71
|
+
agentHandoff: {
|
|
72
|
+
path: sourceTexts.agentHandoff.path,
|
|
73
|
+
exists: sourceTexts.agentHandoff.exists,
|
|
74
|
+
latestCompletedTaskId: agentHandoff.latestCompletedTaskId,
|
|
75
|
+
activeTaskId: agentHandoff.activeTaskId
|
|
76
|
+
},
|
|
77
|
+
taskBoard: {
|
|
78
|
+
path: sourceTexts.taskBoard.path,
|
|
79
|
+
exists: sourceTexts.taskBoard.exists,
|
|
80
|
+
rows: taskBoardRows.length,
|
|
81
|
+
latestDoneTaskId: latestTaskId(taskBoardRows.filter((row) => isDone(row.status)).map((row) => row.id)),
|
|
82
|
+
activeTaskIds
|
|
83
|
+
},
|
|
84
|
+
developmentSlices: {
|
|
85
|
+
path: sourceTexts.developmentSlices.path,
|
|
86
|
+
exists: sourceTexts.developmentSlices.exists,
|
|
87
|
+
latestDoneTaskId: developmentSlices.latestDoneTaskId
|
|
88
|
+
},
|
|
89
|
+
docsRegistry,
|
|
90
|
+
releaseReadiness: {
|
|
91
|
+
path: releaseReadiness.path,
|
|
92
|
+
exists: releaseReadiness.exists
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
tasks: projectedTasks,
|
|
96
|
+
issues
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function toStateProjectionAdvisory(report, issueLimit = 10) {
|
|
100
|
+
const limit = Math.max(0, issueLimit);
|
|
101
|
+
return {
|
|
102
|
+
mode: 'advisory',
|
|
103
|
+
strictBlocking: false,
|
|
104
|
+
consistent: report.summary.consistent,
|
|
105
|
+
issueCounts: report.summary.issueCounts,
|
|
106
|
+
latestDoneTaskId: report.summary.latestDoneTaskId,
|
|
107
|
+
activeTaskIds: report.summary.activeTaskIds,
|
|
108
|
+
checkedTasks: report.summary.checkedTasks,
|
|
109
|
+
issues: report.issues.slice(0, limit),
|
|
110
|
+
truncatedIssues: Math.max(0, report.issues.length - limit)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function formatStateProjectionReport(report, issueLimit = 10) {
|
|
114
|
+
const advisory = toStateProjectionAdvisory(report, issueLimit);
|
|
115
|
+
const counts = `errors ${advisory.issueCounts.error}, warnings ${advisory.issueCounts.warning}, info ${advisory.issueCounts.info}`;
|
|
116
|
+
const lines = [
|
|
117
|
+
'[HADARA] State verify',
|
|
118
|
+
`consistent: ${advisory.consistent}`,
|
|
119
|
+
`issues: ${counts}`,
|
|
120
|
+
`latestDoneTaskId: ${advisory.latestDoneTaskId ?? 'none'}`,
|
|
121
|
+
`activeTaskIds: ${advisory.activeTaskIds.join(', ') || 'none'}`,
|
|
122
|
+
`checkedTasks: ${advisory.checkedTasks}`,
|
|
123
|
+
'rollout: advisory only; strict gates do not block on state projection drift yet.'
|
|
124
|
+
];
|
|
125
|
+
for (const issue of advisory.issues) {
|
|
126
|
+
lines.push(`- ${issue.severity} ${issue.code}: ${issue.message}${issue.path ? ` (${issue.path})` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
if (advisory.truncatedIssues > 0)
|
|
129
|
+
lines.push(`- ... ${advisory.truncatedIssues} more issue(s) omitted`);
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
function readSources(projectRoot, issues) {
|
|
133
|
+
const projectState = readSource(projectRoot, 'docs/PROJECT_STATE.md');
|
|
134
|
+
const agentHandoff = readSource(projectRoot, 'docs/AGENT_HANDOFF.md');
|
|
135
|
+
const taskBoard = readSource(projectRoot, 'docs/TASK_BOARD.md');
|
|
136
|
+
const developmentSlices = readSource(projectRoot, 'docs/DEVELOPMENT_SLICES.md');
|
|
137
|
+
for (const source of [projectState, agentHandoff, taskBoard, developmentSlices]) {
|
|
138
|
+
if (!source.exists)
|
|
139
|
+
issues.push(warning('STATE_SOURCE_MISSING', source.path, `${source.path} is missing.`, `Restore ${source.path} or run the relevant HADARA init/profile remediation.`));
|
|
140
|
+
}
|
|
141
|
+
return { projectState, agentHandoff, taskBoard, developmentSlices };
|
|
142
|
+
}
|
|
143
|
+
function readSource(projectRoot, relativePath) {
|
|
144
|
+
const absolutePath = node_path_1.default.join(projectRoot, relativePath);
|
|
145
|
+
const exists = node_fs_1.default.existsSync(absolutePath);
|
|
146
|
+
return {
|
|
147
|
+
path: relativePath,
|
|
148
|
+
exists,
|
|
149
|
+
content: exists ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : ''
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseTaskBoardRows(content) {
|
|
153
|
+
return (0, markdown_table_1.parseMarkdownRows)(content)
|
|
154
|
+
.filter((row) => /^T-\d{4}$/.test(row[0] ?? ''))
|
|
155
|
+
.map((row) => ({
|
|
156
|
+
id: row[0],
|
|
157
|
+
title: row[1] ?? '',
|
|
158
|
+
status: row[2] ?? '',
|
|
159
|
+
capsule: row[3] ?? ''
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
function buildTaskProjection(projectRoot, taskId, task, taskBoard, deepCheck, issues) {
|
|
163
|
+
const taskPath = task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'TASK.md'))) : '';
|
|
164
|
+
const handoffPath = task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'HANDOFF.md'))) : '';
|
|
165
|
+
const planPath = task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'PLAN.md'))) : '';
|
|
166
|
+
const taskStatus = task ? readTaskStatus(node_path_1.default.join(task.dir, 'TASK.md')) : null;
|
|
167
|
+
const taskHandoff = task && deepCheck ? readTaskHandoff(node_path_1.default.join(task.dir, 'HANDOFF.md')) : { exists: task ? node_fs_1.default.existsSync(node_path_1.default.join(task.dir, 'HANDOFF.md')) : false, taskStatus: null, closeState: null };
|
|
168
|
+
const plan = task && deepCheck ? readPlanState(node_path_1.default.join(task.dir, 'PLAN.md')) : { exists: task ? node_fs_1.default.existsSync(node_path_1.default.join(task.dir, 'PLAN.md')) : false, totalRows: 0, doneRows: 0, pendingRows: 0, inProgressRows: 0 };
|
|
169
|
+
const closeProof = task && deepCheck ? readCloseProof(projectRoot, task) : { path: task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'evidence.jsonl'))) : '', state: 'unknown', sourceHash: null, currentSourceHash: null };
|
|
170
|
+
const capsule = task ? toPortablePath(node_path_1.default.relative(projectRoot, task.dir)) : taskBoard?.capsule ?? '';
|
|
171
|
+
if (!task && taskBoard) {
|
|
172
|
+
issues.push(warning('STATE_TASK_BOARD_CAPSULE_MISSING', 'docs/TASK_BOARD.md', `Task Board row ${taskId} points at ${taskBoard.capsule || '(empty)'}, but no matching capsule was found.`, `Create the missing capsule or update/remove the ${taskId} Task Board row.`, taskId));
|
|
173
|
+
}
|
|
174
|
+
if (task && !taskBoard) {
|
|
175
|
+
issues.push(warning('STATE_TASK_BOARD_ROW_MISSING', 'docs/TASK_BOARD.md', `Task Capsule ${taskId} exists but has no Task Board row.`, `Run task workflow remediation or add a Task Board row for ${taskId}.`, taskId));
|
|
176
|
+
}
|
|
177
|
+
if (task && taskBoard && taskBoard.capsule !== capsule) {
|
|
178
|
+
issues.push(warning('STATE_TASK_BOARD_CAPSULE_DRIFT', 'docs/TASK_BOARD.md', `Task Board capsule for ${taskId} is ${taskBoard.capsule || '(empty)'}, expected ${capsule}.`, `Update the Task Board capsule cell for ${taskId}.`, taskId, capsule, taskBoard.capsule || '(empty)'));
|
|
179
|
+
}
|
|
180
|
+
if (taskStatus && taskBoard?.status && taskStatus !== taskBoard.status) {
|
|
181
|
+
issues.push(warning('STATE_TASK_BOARD_STATUS_DRIFT', 'docs/TASK_BOARD.md', `Task Board status for ${taskId} is ${taskBoard.status}, but TASK.md status is ${taskStatus}.`, `Run hadara task finish --task ${taskId} --execute --json after the capsule is complete, or align the status source intentionally.`, taskId, taskStatus, taskBoard.status));
|
|
182
|
+
}
|
|
183
|
+
if (deepCheck && taskHandoff.taskStatus && !TASK_STATUS_TOKENS.has(taskHandoff.taskStatus)) {
|
|
184
|
+
issues.push(warning('STATE_TASK_HANDOFF_STATUS_INVALID', handoffPath, `Task handoff TaskStatus for ${taskId} is not a canonical task status token: ${taskHandoff.taskStatus}.`, 'Use a canonical TaskStatus token; close proof state belongs in audit-close/proof/status read models.', taskId));
|
|
185
|
+
}
|
|
186
|
+
if (deepCheck && taskHandoff.taskStatus && /pending lifecycle close|closed-valid|not-closed/i.test(taskHandoff.taskStatus)) {
|
|
187
|
+
issues.push(warning('STATE_TASK_HANDOFF_STATUS_CLOSE_STATE_MIXED', handoffPath, `Task handoff TaskStatus for ${taskId} appears to mix task status and close proof state.`, 'Use TaskStatus: Done only; derive CloseState from audit-close/proof/status read models.', taskId));
|
|
188
|
+
}
|
|
189
|
+
if (deepCheck && taskHandoff.closeState) {
|
|
190
|
+
issues.push(warning('STATE_TASK_HANDOFF_CLOSE_STATE_PERSISTED', handoffPath, `Task handoff persists derived CloseState for ${taskId}: ${taskHandoff.closeState}.`, 'Remove CloseState from task-local HANDOFF.md; use audit-close/proof/status read models for derived close state.', taskId));
|
|
191
|
+
if (!CLOSE_STATE_TOKENS.has(taskHandoff.closeState)) {
|
|
192
|
+
issues.push(warning('STATE_TASK_HANDOFF_CLOSE_STATE_INVALID', handoffPath, `Task handoff CloseState for ${taskId} is not canonical: ${taskHandoff.closeState}.`, 'Remove the CloseState row from task-local HANDOFF.md.', taskId));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (deepCheck && (isDone(taskStatus) || isDone(taskBoard?.status)) && (plan.pendingRows > 0 || plan.inProgressRows > 0)) {
|
|
196
|
+
issues.push(warning('STATE_TASK_PLAN_DRIFT', planPath, `Done task ${taskId} has PLAN rows still Pending or In Progress.`, 'Update PLAN.md rows to Done or record an explicit residual-risk decision before closing.', taskId));
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
id: taskId,
|
|
200
|
+
title: task?.title ?? taskBoard?.title ?? 'Unknown',
|
|
201
|
+
capsule,
|
|
202
|
+
task: {
|
|
203
|
+
path: taskPath,
|
|
204
|
+
exists: Boolean(task),
|
|
205
|
+
status: taskStatus
|
|
206
|
+
},
|
|
207
|
+
taskBoard: {
|
|
208
|
+
path: 'docs/TASK_BOARD.md',
|
|
209
|
+
present: Boolean(taskBoard),
|
|
210
|
+
status: taskBoard?.status ?? null,
|
|
211
|
+
capsule: taskBoard?.capsule ?? null
|
|
212
|
+
},
|
|
213
|
+
handoff: {
|
|
214
|
+
path: handoffPath,
|
|
215
|
+
exists: taskHandoff.exists,
|
|
216
|
+
taskStatus: taskHandoff.taskStatus,
|
|
217
|
+
closeState: taskHandoff.closeState
|
|
218
|
+
},
|
|
219
|
+
plan: {
|
|
220
|
+
...plan,
|
|
221
|
+
path: planPath
|
|
222
|
+
},
|
|
223
|
+
closeProof
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function readTaskStatus(taskPath) {
|
|
227
|
+
if (!node_fs_1.default.existsSync(taskPath))
|
|
228
|
+
return null;
|
|
229
|
+
const section = (0, markdown_table_1.readMarkdownSection)(node_fs_1.default.readFileSync(taskPath, 'utf8'), '## Status');
|
|
230
|
+
return section.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
231
|
+
}
|
|
232
|
+
function readTaskHandoff(handoffPath) {
|
|
233
|
+
if (!node_fs_1.default.existsSync(handoffPath))
|
|
234
|
+
return { exists: false, taskStatus: null, closeState: null };
|
|
235
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(node_fs_1.default.readFileSync(handoffPath, 'utf8'), '## Current State');
|
|
236
|
+
return {
|
|
237
|
+
exists: true,
|
|
238
|
+
taskStatus: (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'TaskStatus')?.[1] ?? (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Status')?.[1] ?? null,
|
|
239
|
+
closeState: (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'CloseState')?.[1] ?? null
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function readPlanState(planPath) {
|
|
243
|
+
if (!node_fs_1.default.existsSync(planPath)) {
|
|
244
|
+
return { path: '', exists: false, totalRows: 0, doneRows: 0, pendingRows: 0, inProgressRows: 0 };
|
|
245
|
+
}
|
|
246
|
+
const rows = (0, markdown_table_1.parseMarkdownRows)(node_fs_1.default.readFileSync(planPath, 'utf8')).filter((row) => /^\d+$/.test(row[0] ?? ''));
|
|
247
|
+
return {
|
|
248
|
+
path: toPortablePath(planPath),
|
|
249
|
+
exists: true,
|
|
250
|
+
totalRows: rows.length,
|
|
251
|
+
doneRows: rows.filter((row) => normalizeStatus(row[2]) === 'done').length,
|
|
252
|
+
pendingRows: rows.filter((row) => normalizeStatus(row[2]) === 'pending').length,
|
|
253
|
+
inProgressRows: rows.filter((row) => normalizeStatus(row[2]) === 'inprogress').length
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function readCloseProof(projectRoot, task) {
|
|
257
|
+
const evidencePath = node_path_1.default.join(task.dir, 'evidence.jsonl');
|
|
258
|
+
const relativeEvidencePath = toPortablePath(node_path_1.default.relative(projectRoot, evidencePath));
|
|
259
|
+
const records = (0, evidence_list_1.parseEvidenceIndexFile)(evidencePath, task.id).records.filter((record) => (0, evidence_1.persistedEvidenceKind)(record) === 'command-log' && /Task close validation .* before close evidence append/.test(record.summary));
|
|
260
|
+
const latest = records.at(-1);
|
|
261
|
+
const currentSourceHash = hashCloseRelevantSource(projectRoot, task.dir);
|
|
262
|
+
if (!latest)
|
|
263
|
+
return { path: relativeEvidencePath, state: 'not-closed', sourceHash: null, currentSourceHash };
|
|
264
|
+
const sourceHash = extractSourceHash(latest.summary);
|
|
265
|
+
const passed = (0, evidence_1.persistedEvidenceResult)(latest) === 'passed';
|
|
266
|
+
const state = !passed ? 'closed-invalid' : sourceHash && sourceHash !== currentSourceHash ? 'closed-stale' : 'closed-valid';
|
|
267
|
+
return { path: relativeEvidencePath, state, sourceHash, currentSourceHash };
|
|
268
|
+
}
|
|
269
|
+
function extractProjectState(source) {
|
|
270
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(source.content, '## Metadata');
|
|
271
|
+
return {
|
|
272
|
+
latestCompletedTaskId: extractTaskId((0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Latest Completed Task')?.[1] ?? ''),
|
|
273
|
+
activeTaskId: extractTaskId((0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Active Task')?.[1] ?? '')
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function extractAgentHandoff(source) {
|
|
277
|
+
const currentRows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(source.content, '## Current State');
|
|
278
|
+
const latest = (0, markdown_table_1.findMarkdownRowByCell)(currentRows, 0, 'Latest Completed Task')?.[1] ?? '';
|
|
279
|
+
const active = (0, markdown_table_1.findMarkdownRowByCell)(currentRows, 0, 'Active / Next Task')?.[1] ?? '';
|
|
280
|
+
const lastThreeRows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(source.content, '## Last 3 Completed Tasks');
|
|
281
|
+
return {
|
|
282
|
+
latestCompletedTaskId: extractTaskId(latest) ?? extractTaskId(lastThreeRows.find((row) => /^T-\d{4}/.test(row[0] ?? ''))?.[0] ?? ''),
|
|
283
|
+
activeTaskId: extractTaskId(active)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function extractDevelopmentSlices(source) {
|
|
287
|
+
const rows = (0, markdown_table_1.parseMarkdownRows)(source.content);
|
|
288
|
+
const doneIds = rows
|
|
289
|
+
.filter((row) => /^T-\d{4}$/.test(row[2] ?? '') && /^Done\b/.test(row[4] ?? ''))
|
|
290
|
+
.map((row) => row[2]);
|
|
291
|
+
return { latestDoneTaskId: latestTaskId(doneIds) };
|
|
292
|
+
}
|
|
293
|
+
function extractDocsRegistry(projectRoot, issues) {
|
|
294
|
+
const source = readSource(projectRoot, '.hadara/docs-registry.json');
|
|
295
|
+
if (!source.exists) {
|
|
296
|
+
issues.push(warning('STATE_DOCS_REGISTRY_MISSING', source.path, '.hadara/docs-registry.json is missing.', 'Run docs registry generation or protocol migration before relying on docs state projection.'));
|
|
297
|
+
return { path: source.path, exists: false, registeredDocuments: null, statusCounts: {} };
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(source.content);
|
|
301
|
+
const documents = Array.isArray(parsed.documents) ? parsed.documents : [];
|
|
302
|
+
return {
|
|
303
|
+
path: source.path,
|
|
304
|
+
exists: true,
|
|
305
|
+
registeredDocuments: documents.length,
|
|
306
|
+
statusCounts: documents.reduce((acc, doc) => {
|
|
307
|
+
const status = doc.status ?? 'unknown';
|
|
308
|
+
acc[status] = (acc[status] ?? 0) + 1;
|
|
309
|
+
return acc;
|
|
310
|
+
}, {})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
issues.push(warning('STATE_DOCS_REGISTRY_INVALID_JSON', source.path, `.hadara/docs-registry.json could not be parsed: ${error instanceof Error ? error.message : String(error)}`, 'Repair the docs registry JSON before relying on document state projection.'));
|
|
315
|
+
return { path: source.path, exists: true, registeredDocuments: null, statusCounts: {} };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function compareLatestTask(code, sourcePath, actual, expected, issues) {
|
|
319
|
+
if (!actual || !expected || actual === expected)
|
|
320
|
+
return;
|
|
321
|
+
issues.push(warning(code, sourcePath, `${sourcePath} points to latest completed task ${actual}, but the projected latest Done task is ${expected}.`, `Update ${sourcePath} latest completed task state to ${expected} or correct the Done task source.`, undefined, expected, actual));
|
|
322
|
+
}
|
|
323
|
+
function compareActiveTasks(projectActive, handoffActive, activeTaskIds, issues) {
|
|
324
|
+
const expected = activeTaskIds[0] ?? null;
|
|
325
|
+
if (activeTaskIds.length > 1) {
|
|
326
|
+
issues.push(warning('STATE_MULTIPLE_ACTIVE_TASKS', 'docs/TASK_BOARD.md', `Task Board has multiple active tasks: ${activeTaskIds.join(', ')}.`, 'Keep only one In Progress task unless a future coordinator explicitly supports parallel active work.'));
|
|
327
|
+
}
|
|
328
|
+
if (expected && projectActive && projectActive !== expected) {
|
|
329
|
+
issues.push(warning('STATE_PROJECT_ACTIVE_TASK_MISMATCH', 'docs/PROJECT_STATE.md', `Project State active task is ${projectActive}, but Task Board active task is ${expected}.`, `Update Project State Active Task to ${expected} or finish/reclassify the Task Board row.`, undefined, expected, projectActive));
|
|
330
|
+
}
|
|
331
|
+
if (expected && handoffActive && handoffActive !== expected) {
|
|
332
|
+
issues.push(warning('STATE_HANDOFF_ACTIVE_TASK_MISMATCH', 'docs/AGENT_HANDOFF.md', `Agent Handoff active task is ${handoffActive}, but Task Board active task is ${expected}.`, `Update Agent Handoff Active / Next Task to ${expected} or finish/reclassify the Task Board row.`, undefined, expected, handoffActive));
|
|
333
|
+
}
|
|
334
|
+
if (!expected && projectActive) {
|
|
335
|
+
issues.push(warning('STATE_PROJECT_ACTIVE_TASK_STALE', 'docs/PROJECT_STATE.md', `Project State names active task ${projectActive}, but Task Board has no In Progress task.`, 'Set Active Task to None or mark the active Task Board row In Progress.', undefined, 'None', projectActive));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function checkLatestCloseProof(projectedTasks, latestDoneTaskId, issues) {
|
|
339
|
+
if (!latestDoneTaskId)
|
|
340
|
+
return;
|
|
341
|
+
const task = projectedTasks.find((candidate) => candidate.id === latestDoneTaskId);
|
|
342
|
+
if (!task)
|
|
343
|
+
return;
|
|
344
|
+
if (task.closeProof.state === 'closed-valid')
|
|
345
|
+
return;
|
|
346
|
+
if (task.closeProof.state === 'not-closed') {
|
|
347
|
+
issues.push(warning('STATE_LATEST_CLOSE_PROOF_MISSING', task.closeProof.path, `Latest Done task ${latestDoneTaskId} has no close proof.`, `Run hadara task ready --task ${latestDoneTaskId} --level done --json, then task close/audit-close.`, latestDoneTaskId));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (task.closeProof.state === 'closed-stale') {
|
|
351
|
+
issues.push(warning('STATE_LATEST_CLOSE_PROOF_STALE', task.closeProof.path, `Latest Done task ${latestDoneTaskId} has stale close proof.`, `Rerun hadara task ready --task ${latestDoneTaskId} --level done --json, then task close/audit-close after intentional close-source edits.`, latestDoneTaskId, task.closeProof.currentSourceHash ?? undefined, task.closeProof.sourceHash ?? undefined));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
issues.push(warning('STATE_LATEST_CLOSE_PROOF_INVALID', task.closeProof.path, `Latest Done task ${latestDoneTaskId} close proof is ${task.closeProof.state}.`, `Rerun task close/audit-close for ${latestDoneTaskId} after resolving validation blockers.`, latestDoneTaskId));
|
|
355
|
+
}
|
|
356
|
+
function hashCloseRelevantSource(projectRoot, taskDir) {
|
|
357
|
+
const payload = (0, task_close_1.closeRelevantSourceRelativePaths)(projectRoot, taskDir).map((relativePath) => {
|
|
358
|
+
const absolutePath = node_path_1.default.join(projectRoot, relativePath);
|
|
359
|
+
return {
|
|
360
|
+
path: relativePath,
|
|
361
|
+
exists: node_fs_1.default.existsSync(absolutePath),
|
|
362
|
+
sha256: node_fs_1.default.existsSync(absolutePath) ? node_crypto_1.default.createHash('sha256').update(node_fs_1.default.readFileSync(absolutePath)).digest('hex') : null
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
return `sha256:${node_crypto_1.default.createHash('sha256').update(JSON.stringify(payload), 'utf8').digest('hex')}`;
|
|
366
|
+
}
|
|
367
|
+
function extractSourceHash(summary) {
|
|
368
|
+
return summary.match(/sourceHash\s+(sha256:[a-f0-9]+)/)?.[1] ?? null;
|
|
369
|
+
}
|
|
370
|
+
function extractTaskId(value) {
|
|
371
|
+
return value.match(/\bT-\d{4}\b/)?.[0] ?? null;
|
|
372
|
+
}
|
|
373
|
+
function latestTaskId(ids) {
|
|
374
|
+
return ids.sort().at(-1) ?? null;
|
|
375
|
+
}
|
|
376
|
+
function normalizeStatus(value) {
|
|
377
|
+
return (value ?? '').trim().toLowerCase().replace(/[\s_-]+/g, '');
|
|
378
|
+
}
|
|
379
|
+
function isDone(value) {
|
|
380
|
+
return normalizeStatus(value) === 'done';
|
|
381
|
+
}
|
|
382
|
+
function isActive(value) {
|
|
383
|
+
return normalizeStatus(value) === 'inprogress';
|
|
384
|
+
}
|
|
385
|
+
function countIssues(issues) {
|
|
386
|
+
return {
|
|
387
|
+
error: issues.filter((issue) => issue.severity === 'error').length,
|
|
388
|
+
warning: issues.filter((issue) => issue.severity === 'warning').length,
|
|
389
|
+
info: issues.filter((issue) => issue.severity === 'info').length
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function warning(code, pathValue, message, fixHint, taskId, expected, actual) {
|
|
393
|
+
return {
|
|
394
|
+
severity: 'warning',
|
|
395
|
+
code,
|
|
396
|
+
path: pathValue,
|
|
397
|
+
...(taskId ? { taskId } : {}),
|
|
398
|
+
message,
|
|
399
|
+
...(expected ? { expected } : {}),
|
|
400
|
+
...(actual ? { actual } : {}),
|
|
401
|
+
fixHint
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function toPortablePath(value) {
|
|
405
|
+
return value.split(node_path_1.default.sep).join('/');
|
|
406
|
+
}
|
|
@@ -26,7 +26,7 @@ exports.TASK_FILES = {
|
|
|
26
26
|
'DECISIONS.md': () => `# Decisions\n\n| ID | Decision | Status | Rationale | Evidence |\n|---|---|---|---|---|\n`,
|
|
27
27
|
'EVIDENCE.md': () => `# Evidence\n\n| Time | Kind | Summary | Result | Visibility | JSONL |\n|---|---|---|---|---|---|\n`,
|
|
28
28
|
'evidence.jsonl': () => '',
|
|
29
|
-
'HANDOFF.md': (task) => `# Handoff\n\n## Current State\n\n${(0, managed_sections_1.managedSectionBlock)('task-handoff-current-state', { schema: 'hadara.managedSection.v1', owner: 'handoff.update', kind: 'key-value-table', mode: 'update-row', version: 1, required: true, closeSourceRole: 'included' }, `| Field | Value |\n|---|---|\n| Task | ${task.id} |\n|
|
|
29
|
+
'HANDOFF.md': (task) => `# Handoff\n\n## Current State\n\n${(0, managed_sections_1.managedSectionBlock)('task-handoff-current-state', { schema: 'hadara.managedSection.v1', owner: 'handoff.update', kind: 'key-value-table', mode: 'update-row', version: 1, required: true, closeSourceRole: 'included' }, `| Field | Value |\n|---|---|\n| Task | ${task.id} |\n| TaskStatus | Draft |\n| Last Updated | TBD |\n`)}\n\n## Last Completed\n\n| Item | Evidence |\n|---|---|\n| TBD | TBD |\n\n## Next Recommended Step\n\n| Step | Reason | Required Reading |\n|---|---|---|\n| TBD | TBD | TBD |\n\n## Carry Forward Warnings\n\n| Warning | Impact | Mitigation |\n|---|---|---|\n`
|
|
30
30
|
};
|
|
31
31
|
function isTaskCapsuleScaffoldContent(task, fileName, content) {
|
|
32
32
|
if (fileName === 'TASK.md') {
|
|
@@ -146,7 +146,11 @@ function listTaskCapsules(projectRoot) {
|
|
|
146
146
|
return [];
|
|
147
147
|
return node_fs_1.default
|
|
148
148
|
.readdirSync(tasksDir, { withFileTypes: true })
|
|
149
|
-
.filter((entry) =>
|
|
149
|
+
.filter((entry) => {
|
|
150
|
+
if (!entry.isDirectory() || !/^T-\d{4}-/.test(entry.name))
|
|
151
|
+
return false;
|
|
152
|
+
return node_fs_1.default.existsSync(node_path_1.default.join(tasksDir, entry.name, 'TASK.md'));
|
|
153
|
+
})
|
|
150
154
|
.map((entry) => {
|
|
151
155
|
const [id, ...slugParts] = entry.name.split('-');
|
|
152
156
|
const number = slugParts.shift();
|
|
@@ -169,7 +173,9 @@ function findTaskCapsule(projectRoot, taskId) {
|
|
|
169
173
|
return undefined;
|
|
170
174
|
const entry = node_fs_1.default
|
|
171
175
|
.readdirSync(tasksDir, { withFileTypes: true })
|
|
172
|
-
.find((candidate) => candidate.isDirectory() &&
|
|
176
|
+
.find((candidate) => candidate.isDirectory() &&
|
|
177
|
+
candidate.name.startsWith(`${taskId}-`) &&
|
|
178
|
+
node_fs_1.default.existsSync(node_path_1.default.join(tasksDir, candidate.name, 'TASK.md')));
|
|
173
179
|
if (!entry)
|
|
174
180
|
return undefined;
|
|
175
181
|
const slug = entry.name.slice(`${taskId}-`.length);
|