hadara 0.3.1-rc.1 → 0.3.2-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1--rc.1-orange">
9
+ <img alt="Source version" src="https://img.shields.io/badge/source-0.3.2--rc.0-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>
@@ -30,18 +30,18 @@ hadara@0.3.0
30
30
  Current release candidate:
31
31
 
32
32
  ```text
33
- hadara@0.3.1-rc.1
33
+ hadara@0.3.2-rc.0
34
34
  ```
35
35
 
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.
36
+ The 0.3.2-rc.0 line is the Evidence v2 refactor release candidate. It hardens evidence writer metadata, exact resolution markers, durable evidence id discovery, canonical/derived evidence boundaries, and release-facing docs after the 0.3.1 post-publish recycle.
37
37
 
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.
38
+ Phase labels are internal implementation phases, not npm release-candidate labels. The stable `0.3.0` package remains the default install target; the `0.3.2-rc.0` package is for explicit release-candidate evaluation.
39
39
 
40
40
  | Surface | Status |
41
41
  |---|---|
42
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) |
43
+ | Current RC | [`hadara@0.3.2-rc.0`](docs/RELEASE_NOTES.md#032-rc0) |
44
+ | Previous RC | [`hadara@0.3.1-rc.1`](docs/RELEASE_NOTES.md#031-rc1) |
45
45
  | Historical RCs | See [Release Notes](docs/RELEASE_NOTES.md). |
46
46
  | GitHub Release | Secondary target, approval-gated. |
47
47
  | Docker image | Deferred. |
@@ -72,8 +72,8 @@ npx hadara@0.3.0 doctor --json
72
72
  Evaluate the release candidate explicitly:
73
73
 
74
74
  ```bash
75
- npm install -g hadara@0.3.1-rc.1
76
- npx hadara@0.3.1-rc.1 help
75
+ npm install -g hadara@0.3.2-rc.0
76
+ npx hadara@0.3.2-rc.0 help
77
77
  ```
78
78
 
79
79
  For release or recycle evidence, prefer an isolated prefix install when PATH, global installs, or `npx` cache behavior may be stale:
@@ -128,7 +128,7 @@ The primary path is intentionally small:
128
128
  hadara task next --json
129
129
  hadara task create "implement a focused change" --json
130
130
  hadara task status --task T-XXXX --json
131
- hadara evidence add-command --task T-XXXX --summary "..." --result passed --idempotency-key "command:T-XXXX:check" --json
131
+ hadara evidence add-command --task T-XXXX --summary "..." --result passed --category validation --idempotency-key "command:T-XXXX:check" --json
132
132
  hadara task finish --task T-XXXX --json
133
133
  hadara task finish --task T-XXXX --execute --json
134
134
  # Finalize Task Capsule docs and tracked state docs before closing.
@@ -139,6 +139,19 @@ hadara task audit-close --task T-XXXX --json
139
139
  hadara handoff suggest --task T-XXXX --json
140
140
  ```
141
141
 
142
+ When `evidence add-command` uses both legacy `--result` and v2 `--outcome`, matching outcomes must agree with the legacy result. `recorded` and `not-applicable` outcomes keep legacy result `unknown`; incompatible combinations fail before evidence is appended.
143
+
144
+ Use `hadara evidence list --task T-XXXX` to discover evidence ids before writing exact resolution markers. Text output shows `[id] time | category/outcome | visibility | summary`; JSON output includes `id`, `idSource`, `idStability`, `persistedSchemaVersion`, `category`, `outcome`, and `tags`. For long-lived references, copy only durable persisted `ev:` ids:
145
+
146
+ ```bash
147
+ hadara evidence list --task T-XXXX
148
+ hadara evidence add-command --task T-XXXX --summary "Fix verified" --result passed --category validation --resolves ev:T-XXXX:aaaaaaaaaaaaaaaaaaaaaaaa --json
149
+ ```
150
+
151
+ Legacy compatibility ids are inspection-only and are not the preferred durable reference for `resolves:` or `supersedes:` examples.
152
+
153
+ Deferred Evidence v2 scope is explicit: rebuild preview/execute, `check-id`, `subject`, and a new add-command report schema id are future candidates, not current command behavior. Treat `evidence.jsonl` as canonical append-only evidence and `EVIDENCE.md` as a non-canonical human summary.
154
+
142
155
  Optional workflow compression is read-only. Use it separately when you want a compact current-stage report and next recommended action:
143
156
 
144
157
  ```bash
@@ -232,7 +245,7 @@ Dashboard, TUI, Hermes, MCP, installer, package, release, and run commands stay
232
245
 
233
246
  ## Safety Boundaries
234
247
 
235
- HADARA 0.3.1-rc.1 is not:
248
+ HADARA 0.3.2-rc.0 is not:
236
249
 
237
250
  - a full agent runtime;
238
251
  - Rack/enterprise behavior;
@@ -268,6 +281,12 @@ AGENTS.md
268
281
 
269
282
  Portable/local state is not committed. Project docs, Task Capsules, and reduced public evidence are committed when they represent reproducible context.
270
283
 
284
+ ### Evidence Rebuild Boundary
285
+
286
+ `evidence.jsonl` is the canonical Task Capsule evidence source. `EVIDENCE.md` is a non-canonical human summary that can help review validation history, but it must not be treated as the source of truth for rebuild, migration, or resolution logic.
287
+
288
+ 0.3.2 does not implement `hadara evidence rebuild --json` or an execute mode. Future rebuild work must first define whether a difference is formatting regeneration, managed-section drift, or data inconsistency before reporting `wouldChange`. Any later write-capable rebuild flow must be dry-run-first, reviewed, and before-hash guarded before it rewrites derived Markdown.
289
+
271
290
  ## Development / Contributing
272
291
 
273
292
  Initialize a project:
@@ -33,11 +33,18 @@ function createEvidenceCollectReport(projectRoot, input) {
33
33
  summary: input.summary,
34
34
  result: input.result,
35
35
  visibility: input.visibility,
36
+ category: input.category,
37
+ outcome: input.outcome,
38
+ tags: input.tags,
36
39
  idempotencyKey: input.idempotencyKey
37
40
  });
38
41
  }
39
42
  catch (error) {
40
- if (error instanceof workspace_1.WorkspaceFileError || error instanceof evidence_1.EvidenceArtifactPolicyError || error instanceof evidence_1.EvidenceAppendLockError) {
43
+ if (error instanceof workspace_1.WorkspaceFileError ||
44
+ error instanceof evidence_1.EvidenceArtifactPolicyError ||
45
+ error instanceof evidence_1.EvidenceAppendLockError ||
46
+ error instanceof evidence_1.EvidenceResultOutcomeMismatchError ||
47
+ error instanceof evidence_1.EvidenceTaskDirectoryError) {
41
48
  return {
42
49
  schemaVersion: 'hadara.evidence.collect.v1',
43
50
  command: 'evidence.collect',
@@ -4,6 +4,8 @@ exports.handleEvidenceCommand = handleEvidenceCommand;
4
4
  exports.parseEvidenceKind = parseEvidenceKind;
5
5
  exports.parseEvidenceResult = parseEvidenceResult;
6
6
  exports.parseEvidenceVisibility = parseEvidenceVisibility;
7
+ exports.parseEvidenceCategory = parseEvidenceCategory;
8
+ exports.parseEvidenceOutcome = parseEvidenceOutcome;
7
9
  const evidence_1 = require("../evidence/evidence");
8
10
  const evidence_json_1 = require("./evidence-json");
9
11
  const evidence_lint_1 = require("../services/evidence-lint");
@@ -24,7 +26,7 @@ function handleEvidenceCommand(input) {
24
26
  }
25
27
  else {
26
28
  for (const record of report.records) {
27
- console.log(`${record.time} | ${(0, evidence_1.persistedEvidenceKind)(record)} | ${(0, evidence_1.persistedEvidenceResult)(record)} | ${record.visibility} | ${record.summary}`);
29
+ console.log(`[${evidenceListDisplayId(record)}] ${record.time} | ${evidenceListCategory(record)}/${evidenceListOutcome(record)} | ${record.visibility} | ${record.summary}`);
28
30
  }
29
31
  for (const issue of report.issues) {
30
32
  console.log(`[${issue.severity}] ${issue.code}: ${issue.message}`);
@@ -76,9 +78,30 @@ function handleEvidenceCommand(input) {
76
78
  if (sub === 'add-command') {
77
79
  const taskId = (0, args_1.getRequiredStringOption)(input.args, '--task');
78
80
  const summary = (0, args_1.getStringOption)(input.args, '--summary') ?? 'Command completed.';
79
- const result = parseEvidenceResult((0, args_1.getStringOption)(input.args, '--result', 'unknown') ?? 'unknown');
81
+ const outcome = parseOptionalEvidenceOutcome((0, args_1.getStringOption)(input.args, '--outcome'));
82
+ const explicitResult = (0, args_1.getStringOption)(input.args, '--result');
83
+ const result = parseEvidenceResult(explicitResult ?? outcomeToLegacyResult(outcome));
84
+ const resultOutcomeIssue = (0, evidence_1.validateEvidenceResultOutcomeCompatibility)({ result, outcome });
85
+ if (resultOutcomeIssue) {
86
+ const report = {
87
+ schemaVersion: 'hadara.evidence.collect.v1',
88
+ command: 'evidence.add-command',
89
+ ok: false,
90
+ issues: [resultOutcomeIssue]
91
+ };
92
+ if (input.jsonOutput) {
93
+ console.log(JSON.stringify(report, null, 2));
94
+ }
95
+ else {
96
+ console.log(`[error] ${resultOutcomeIssue.code}: ${resultOutcomeIssue.message}`);
97
+ }
98
+ process.exitCode = 6;
99
+ return true;
100
+ }
80
101
  const visibility = parseEvidenceVisibility((0, args_1.getStringOption)(input.args, '--visibility', 'public') ?? 'public', (0, args_1.getFlag)(input.args, '--private'));
81
102
  const idempotencyKey = (0, args_1.getStringOption)(input.args, '--idempotency-key');
103
+ const category = parseOptionalEvidenceCategory((0, args_1.getStringOption)(input.args, '--category'));
104
+ const tags = resolutionTagsFromArgs(input.args);
82
105
  if (input.jsonOutput) {
83
106
  const report = (0, evidence_json_1.createEvidenceCollectReport)(input.projectRoot, {
84
107
  taskId,
@@ -86,6 +109,9 @@ function handleEvidenceCommand(input) {
86
109
  summary,
87
110
  result,
88
111
  visibility,
112
+ category,
113
+ outcome,
114
+ tags,
89
115
  idempotencyKey
90
116
  });
91
117
  console.log(JSON.stringify({ ...report, command: 'evidence.add-command' }, null, 2));
@@ -93,7 +119,7 @@ function handleEvidenceCommand(input) {
93
119
  process.exitCode = 6;
94
120
  }
95
121
  else {
96
- const appendResult = (0, evidence_1.appendEvidenceWithResult)(input.projectRoot, { taskId, kind: 'command-log', summary, result, visibility, idempotencyKey });
122
+ const appendResult = (0, evidence_1.appendEvidenceWithResult)(input.projectRoot, { taskId, kind: 'command-log', summary, result, visibility, category, outcome, tags, idempotencyKey });
97
123
  if (appendResult.existing) {
98
124
  console.log(`[HADARA] Command evidence already exists: ${persistedEvidenceId(appendResult.evidence)}`);
99
125
  }
@@ -140,6 +166,15 @@ function handleEvidenceCommand(input) {
140
166
  function persistedEvidenceId(record) {
141
167
  return record.schemaVersion === 'hadara.evidence.v2' && record.id ? record.id : 'evidence.jsonl';
142
168
  }
169
+ function evidenceListDisplayId(record) {
170
+ return record.id;
171
+ }
172
+ function evidenceListCategory(record) {
173
+ return record.category;
174
+ }
175
+ function evidenceListOutcome(record) {
176
+ return record.outcome;
177
+ }
143
178
  function parseEvidenceKind(value) {
144
179
  if (['test-log', 'command-log', 'diff-summary', 'screenshot', 'note'].includes(value)) {
145
180
  return value;
@@ -159,3 +194,46 @@ function parseEvidenceVisibility(value, privateFlag = false) {
159
194
  return value;
160
195
  throw new Error(`unsupported evidence visibility: ${value}`);
161
196
  }
197
+ function parseEvidenceCategory(value) {
198
+ if (['validation', 'implementation', 'release', 'security', 'policy', 'operation', 'decision', 'handoff', 'audit', 'note', 'observation'].includes(value)) {
199
+ return value;
200
+ }
201
+ throw new Error(`unsupported evidence category: ${value}`);
202
+ }
203
+ function parseEvidenceOutcome(value) {
204
+ if (value === 'passed' || value === 'failed' || value === 'blocked' || value === 'unknown' || value === 'recorded' || value === 'not-applicable') {
205
+ return value;
206
+ }
207
+ throw new Error(`unsupported evidence outcome: ${value}`);
208
+ }
209
+ function parseOptionalEvidenceCategory(value) {
210
+ return value ? parseEvidenceCategory(value) : undefined;
211
+ }
212
+ function parseOptionalEvidenceOutcome(value) {
213
+ return value ? parseEvidenceOutcome(value) : undefined;
214
+ }
215
+ function outcomeToLegacyResult(outcome) {
216
+ if (outcome === 'passed' || outcome === 'failed' || outcome === 'blocked' || outcome === 'unknown')
217
+ return outcome;
218
+ return 'unknown';
219
+ }
220
+ function resolutionTagsFromArgs(args) {
221
+ const tags = [];
222
+ for (const id of getRepeatedStringOptions(args, '--resolves'))
223
+ tags.push(`resolves:${id}`);
224
+ for (const id of getRepeatedStringOptions(args, '--supersedes'))
225
+ tags.push(`supersedes:${id}`);
226
+ return tags.length > 0 ? tags : undefined;
227
+ }
228
+ function getRepeatedStringOptions(args, option) {
229
+ const values = [];
230
+ for (let index = 0; index < args.length; index += 1) {
231
+ if (args[index] !== option)
232
+ continue;
233
+ const value = args[index + 1];
234
+ if (!value || value.startsWith('--'))
235
+ throw new Error(`missing value for ${option}`);
236
+ values.push(value);
237
+ }
238
+ return values;
239
+ }
package/dist/cli/init.js CHANGED
@@ -1118,7 +1118,7 @@ hadara task status --task T-XXXX --json
1118
1118
 
1119
1119
  # Do the scoped work.
1120
1120
 
1121
- hadara evidence add-command --task T-XXXX --summary "..." --result passed --idempotency-key "command:T-XXXX:check" --json
1121
+ hadara evidence add-command --task T-XXXX --summary "..." --result passed --category validation --idempotency-key "command:T-XXXX:check" --json
1122
1122
 
1123
1123
  hadara task finish --task T-XXXX --json
1124
1124
  hadara task finish --task T-XXXX --execute --json
@@ -1140,7 +1140,7 @@ hadara task audit-close --task T-XXXX --json
1140
1140
  |---|---|---|
1141
1141
  | \`task next\` | Read-only | Recommends work; does not create tasks. |
1142
1142
  | \`task status\` | Read-only | \`ok\` means report generation succeeded; readiness is in \`state.ready\`, \`summary.blockers\`, and \`issues\`. |
1143
- | \`evidence add-command\` | Write | Appends command-log evidence; does not execute shell commands; optional \`--idempotency-key\` prevents duplicate same-key records. |
1143
+ | \`evidence add-command\` | Write | Appends command-log evidence; does not execute shell commands; optional \`--category\`/\`--outcome\`/\`--resolves\`/\`--supersedes\` enrich v2 metadata, result/outcome mismatches are rejected, and optional \`--idempotency-key\` prevents duplicate same-key records. |
1144
1144
  | \`task ready\` | Read-only | Checks readiness; does not mutate evidence or status docs. |
1145
1145
  | \`task finish\` | Dry-run by default; writes only with \`--execute\` | Bounded to \`TASK.md\` and \`docs/TASK_BOARD.md\`. |
1146
1146
  | \`task close\` | Dry-run by default; writes only with \`--execute\` | Bounded to close evidence append. |
@@ -1165,8 +1165,10 @@ Before running \`task ready\` and \`task close\`, finish all close-source edits:
1165
1165
  1. Do not hand-edit Task Capsule \`evidence.jsonl\`.
1166
1166
  2. Append evidence through HADARA commands so schema, visibility, and artifact-safety checks run consistently.
1167
1167
  3. Record failed or blocked checks honestly. Do not replace them later with optimistic summaries; add newer evidence that explains the fix or residual risk.
1168
- 4. Use \`hadara evidence add-command --task <task-id> --summary <text> --result passed|failed|blocked|unknown --json\` for command results when no artifact file is attached. Add \`--idempotency-key <key>\` when rerunning the same logical check should report one durable evidence identity instead of appending duplicates.
1169
- 5. Use \`hadara evidence lint --task <task-id> --json\` when evidence drift is suspected or before close if evidence files were touched manually by mistake.
1168
+ 4. Use \`hadara evidence list --task <task-id>\` to discover evidence ids and copy durable persisted \`ev:\` ids for long-lived \`--resolves\` or \`--supersedes\` references. Legacy compatibility ids are inspection-only and are not the preferred durable reference.
1169
+ 5. Use \`hadara evidence add-command --task <task-id> --summary <text> --result passed|failed|blocked|unknown --json\` for command results when no artifact file is attached. Add \`--category <category>\` or \`--outcome <outcome>\` when summary heuristics are not precise enough; if both \`--result\` and \`--outcome\` are supplied, matching outcomes must match the legacy result, while \`recorded\` and \`not-applicable\` require \`--result unknown\` or no explicit result. Use \`--resolves <ev:evidence-id>\` or \`--supersedes <ev:evidence-id>\` for exact v2 resolution markers from passed or recorded follow-up evidence, and \`--idempotency-key <key>\` when rerunning the same logical check should report one durable evidence identity instead of appending duplicates.
1170
+ 6. Use \`hadara evidence lint --task <task-id> --json\` when evidence drift is suspected or before close if evidence files were touched manually by mistake.
1171
+ 7. Treat Task Capsule \`evidence.jsonl\` as canonical append-only evidence and \`EVIDENCE.md\` as a non-canonical human summary. \`hadara evidence rebuild\` is not implemented in this scaffold; future rebuild work must define whether \`wouldChange\` means formatting regeneration, managed-section drift, or data inconsistency before preview semantics are safe, and any execute mode must be dry-run-first and before-hash guarded.
1170
1172
 
1171
1173
  ## Session End
1172
1174
 
@@ -1336,7 +1338,7 @@ hadara task status --task T-XXXX --json
1336
1338
 
1337
1339
  # Do the scoped work.
1338
1340
 
1339
- hadara evidence add-command --task T-XXXX --summary "..." --result passed --idempotency-key "command:T-XXXX:check" --json
1341
+ hadara evidence add-command --task T-XXXX --summary "..." --result passed --category validation --idempotency-key "command:T-XXXX:check" --json
1340
1342
 
1341
1343
  hadara task finish --task T-XXXX --json
1342
1344
  hadara task finish --task T-XXXX --execute --json
@@ -1417,7 +1419,7 @@ Evidence outcome tokens are \`passed\`, \`failed\`, \`blocked\`, and \`unknown\`
1417
1419
  |---|---|
1418
1420
  | \`TASK.md\` status metadata, \`## Status\`, and Status History | Command-owned for finish bookkeeping; worker-owned before finish. |
1419
1421
  | \`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\`. |
1422
+ | \`EVIDENCE.md\` and \`evidence.jsonl\` | Evidence writer-owned; do not hand-edit \`evidence.jsonl\`. Treat \`evidence.jsonl\` as canonical and \`EVIDENCE.md\` as a non-canonical human summary; evidence rebuild is not implemented in this scaffold and any future execute mode must be dry-run-first and before-hash guarded. |
1421
1423
  | \`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
1424
  | Shared state docs | Mixed/human-owned; update before close when they are close-source relevant. |
1423
1425
  | \`.hadara/docs-registry.json\` and \`docs/DOC_REGISTRY.md\` | Docs registry-owned; registry mutations should stay dry-run-first or explicitly scoped. |
@@ -1439,7 +1441,7 @@ Serialize same-file writes, evidence append, Task Capsule doc writes, Task Board
1439
1441
  | \`task next\` | Read-only | Recommends work; does not create tasks. |
1440
1442
  | \`task status\` | Read-only | \`ok\` means report generation succeeded; readiness is in \`state.ready\`, \`summary.blockers\`, and \`issues\`. |
1441
1443
  | \`task create\` | Write | Creates a Draft Task Capsule and Task Board row. It does not imply the task is ready or done. |
1442
- | \`evidence add-command\` | Write | Appends operator-supplied command-log evidence. It does not execute shell commands or capture stdout/stderr; optional \`--idempotency-key\` prevents duplicate same-key records. |
1444
+ | \`evidence add-command\` | Write | Appends operator-supplied command-log evidence. It does not execute shell commands or capture stdout/stderr; optional \`--category\`/\`--outcome\`/\`--resolves\`/\`--supersedes\` enrich v2 metadata, result/outcome mismatches are rejected, and optional \`--idempotency-key\` prevents duplicate same-key records. |
1443
1445
  | \`task finish\` | Dry-run by default; writes only with \`--execute\` | Updates only \`TASK.md\` status bookkeeping and the matching \`docs/TASK_BOARD.md\` row. |
1444
1446
  | \`task ready\` | Read-only | Checks whether the task can satisfy the requested readiness level after finish. |
1445
1447
  | \`task complete\` | Read-only | Summarizes the current completion stage and next command; it does not execute lifecycle writes. |
@@ -1453,7 +1455,9 @@ Serialize same-file writes, evidence append, Task Capsule doc writes, Task Board
1453
1455
  - \`task ready\` checks readiness; it does not write evidence or status.
1454
1456
  - \`harness validate\` is a direct diagnostic for Task Capsule structure and done-level gates; it is not a replacement for close evidence.
1455
1457
  - \`task complete\` is a read-only workflow compressor. It may report the next lifecycle command, but it must not execute finish, ready, close, or audit commands.
1456
- - \`evidence add-command\` records an operator-supplied command result; it does not run the command. \`--idempotency-key\` is optional; when supplied, same-key repeats return the existing record without appending duplicate Markdown or JSONL rows.
1458
+ - \`evidence list\` is the supported evidence id discovery surface. Text output shows \`[id] time | category/outcome | visibility | summary\`; JSON records expose \`id\`, \`idSource\`, \`idStability\`, \`persistedSchemaVersion\`, \`category\`, \`outcome\`, and \`tags\`. Use durable persisted \`ev:\` ids for long-lived \`--resolves\` and \`--supersedes\` references. Legacy compatibility ids are inspection-only and are not the preferred durable reference.
1459
+ - \`evidence add-command\` records an operator-supplied command result; it does not run the command. \`--category\` and \`--outcome\` set persisted v2 metadata explicitly, while \`--result\` remains the legacy-compatible command result. When both are supplied, \`--result\` must match \`--outcome\` for \`passed\`, \`failed\`, \`blocked\`, and \`unknown\`; \`recorded\` and \`not-applicable\` require \`--result unknown\` or no explicit \`--result\`. \`--resolves\` and \`--supersedes\` append exact v2 resolution tags from passed or recorded follow-up evidence. \`--idempotency-key\` is optional; when supplied, same-key repeats return the existing record without appending duplicate Markdown or JSONL rows.
1460
+ - Evidence v2 deferred scope remains explicit: rebuild preview/execute, \`check-id\`, \`subject\`, and a new add-command report schema id are future candidates. Do not infer those commands or schema changes from the current \`evidence list\` and \`evidence add-command\` ergonomics.
1457
1461
  - \`task finish\` may update only the Task Capsule \`TASK.md\` status and the matching \`docs/TASK_BOARD.md\` row's command-owned cells: \`ID\`, \`Title\`, \`Status\`, and \`Capsule\`. It preserves human/mixed-owned \`Notes\` and any extra cells.
1458
1462
  - \`task close\` may append only close evidence. It must not update status docs, Task Board rows, handoff, Project State, roadmap docs, or arbitrary evidence.
1459
1463
  - After \`task close --execute --json\`, close-source document edits intentionally invalidate the previous close proof. Make those edits before close, or rerun ready/close/audit if the edit is unavoidable.
@@ -3,10 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.EvidenceAppendLockError = exports.EvidenceArtifactPolicyError = void 0;
6
+ exports.EvidenceTaskDirectoryError = exports.EvidenceResultOutcomeMismatchError = exports.EvidenceAppendLockError = exports.EvidenceArtifactPolicyError = void 0;
7
7
  exports.persistedEvidenceKind = persistedEvidenceKind;
8
8
  exports.persistedEvidenceResult = persistedEvidenceResult;
9
9
  exports.persistedEvidencePath = persistedEvidencePath;
10
+ exports.validateEvidenceResultOutcomeCompatibility = validateEvidenceResultOutcomeCompatibility;
10
11
  exports.appendEvidence = appendEvidence;
11
12
  exports.appendEvidenceWithResult = appendEvidenceWithResult;
12
13
  exports.appendEvidenceTextArtifact = appendEvidenceTextArtifact;
@@ -48,13 +49,53 @@ class EvidenceAppendLockError extends Error {
48
49
  }
49
50
  }
50
51
  exports.EvidenceAppendLockError = EvidenceAppendLockError;
52
+ class EvidenceResultOutcomeMismatchError extends Error {
53
+ issue;
54
+ code = 'EVIDENCE_RESULT_OUTCOME_MISMATCH';
55
+ constructor(issue) {
56
+ super(issue.message);
57
+ this.issue = issue;
58
+ this.name = 'EvidenceResultOutcomeMismatchError';
59
+ }
60
+ }
61
+ exports.EvidenceResultOutcomeMismatchError = EvidenceResultOutcomeMismatchError;
62
+ class EvidenceTaskDirectoryError extends Error {
63
+ code;
64
+ constructor(code, message) {
65
+ super(message);
66
+ this.code = code;
67
+ this.name = 'EvidenceTaskDirectoryError';
68
+ }
69
+ }
70
+ exports.EvidenceTaskDirectoryError = EvidenceTaskDirectoryError;
71
+ function validateEvidenceResultOutcomeCompatibility(input) {
72
+ if (!input.outcome)
73
+ return undefined;
74
+ if (input.outcome === 'passed' || input.outcome === 'failed' || input.outcome === 'blocked' || input.outcome === 'unknown') {
75
+ if (input.result === input.outcome)
76
+ return undefined;
77
+ return {
78
+ severity: 'error',
79
+ code: 'EVIDENCE_RESULT_OUTCOME_MISMATCH',
80
+ message: `Evidence result ${input.result} conflicts with outcome ${input.outcome}; matching evidence outcomes must use the same legacy result.`
81
+ };
82
+ }
83
+ if ((input.outcome === 'recorded' || input.outcome === 'not-applicable') && input.result !== 'unknown') {
84
+ return {
85
+ severity: 'error',
86
+ code: 'EVIDENCE_RESULT_OUTCOME_MISMATCH',
87
+ message: `Evidence outcome ${input.outcome} is record-only/non-applicable evidence and requires legacy result unknown.`
88
+ };
89
+ }
90
+ return undefined;
91
+ }
51
92
  function appendEvidence(projectRoot, record) {
52
93
  return appendEvidenceWithResult(projectRoot, record).markdownPath;
53
94
  }
54
95
  function appendEvidenceWithResult(projectRoot, record) {
55
96
  const taskDir = findTaskDir(projectRoot, record.taskId);
56
97
  if (!taskDir) {
57
- throw new Error(`Task capsule not found: ${record.taskId}`);
98
+ throw new EvidenceTaskDirectoryError('TASK_NOT_FOUND', `Task Capsule not found: ${record.taskId}`);
58
99
  }
59
100
  const time = new Date().toISOString();
60
101
  const visibility = record.visibility ?? 'public';
@@ -70,7 +111,7 @@ function appendEvidenceWithResult(projectRoot, record) {
70
111
  function appendEvidenceTextArtifact(projectRoot, record, artifact, options = {}) {
71
112
  const taskDir = findTaskDir(projectRoot, record.taskId);
72
113
  if (!taskDir) {
73
- throw new Error(`Task capsule not found: ${record.taskId}`);
114
+ throw new EvidenceTaskDirectoryError('TASK_NOT_FOUND', `Task Capsule not found: ${record.taskId}`);
74
115
  }
75
116
  const time = new Date().toISOString();
76
117
  const visibility = record.visibility ?? 'public';
@@ -182,6 +223,13 @@ function sleepSync(ms) {
182
223
  Atomics.wait(signal, 0, 0, ms);
183
224
  }
184
225
  function appendEvidenceRecord(input) {
226
+ const resultOutcomeIssue = validateEvidenceResultOutcomeCompatibility({
227
+ result: input.record.result,
228
+ outcome: input.record.outcome
229
+ });
230
+ if (resultOutcomeIssue) {
231
+ throw new EvidenceResultOutcomeMismatchError(resultOutcomeIssue);
232
+ }
185
233
  const summary = (0, redaction_1.redactSecrets)(input.record.summary.replace(/\|/g, '/'));
186
234
  const markdownPath = node_path_1.default.join(input.taskDir, 'EVIDENCE.md');
187
235
  return withEvidenceAppendLock(input.projectRoot, input.record.taskId, () => {
@@ -212,6 +260,8 @@ function appendEvidenceRecord(input) {
212
260
  kind: input.record.kind,
213
261
  summary,
214
262
  result: input.record.result,
263
+ category: input.record.category,
264
+ outcome: input.record.outcome,
215
265
  visibility: input.visibility,
216
266
  attachedPath,
217
267
  tags: input.record.tags,
@@ -250,8 +300,8 @@ function createEvidenceV2Record(input) {
250
300
  schemaVersion: 'hadara.evidence.v2',
251
301
  time: input.time,
252
302
  taskId: input.taskId,
253
- category: deriveEvidenceCategory(input.kind, input.summary),
254
- outcome: normalizeEvidenceOutcome(input.result),
303
+ category: input.category ?? deriveEvidenceCategory(input.kind, input.summary),
304
+ outcome: input.outcome ?? normalizeEvidenceOutcome(input.result),
255
305
  visibility: input.visibility,
256
306
  summary: input.summary,
257
307
  artifacts: input.visibility === 'public' && input.attachedPath
@@ -375,8 +425,13 @@ function findTaskDir(projectRoot, taskId) {
375
425
  const tasksDir = node_path_1.default.join(projectRoot, 'tasks');
376
426
  if (!node_fs_1.default.existsSync(tasksDir))
377
427
  return null;
378
- const entry = node_fs_1.default.readdirSync(tasksDir).find((name) => name.startsWith(`${taskId}-`));
379
- return entry ? node_path_1.default.join(tasksDir, entry) : null;
428
+ const entries = node_fs_1.default
429
+ .readdirSync(tasksDir, { withFileTypes: true })
430
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${taskId}-`) && node_fs_1.default.existsSync(node_path_1.default.join(tasksDir, entry.name, 'TASK.md')));
431
+ if (entries.length > 1) {
432
+ throw new EvidenceTaskDirectoryError('TASK_CAPSULE_AMBIGUOUS', `Multiple Task Capsules found for ${taskId}; remove or repair duplicate TASK.md-bearing directories before recording evidence.`);
433
+ }
434
+ return entries[0] ? node_path_1.default.join(tasksDir, entries[0].name) : null;
380
435
  }
381
436
  function createSessionEvidenceDirs(dataRoot, sessionId) {
382
437
  const evidenceDir = node_path_1.default.join(dataRoot, 'sessions', sessionId, 'evidence');
@@ -82,10 +82,10 @@ function findUnresolvedFailedEvidence(records, taskDocs = {}) {
82
82
  if (record.outcome !== 'failed')
83
83
  return false;
84
84
  const laterRecords = records.slice(index + 1);
85
- if (laterRecords.some((candidate) => candidate.outcome === 'passed' && candidate.category === record.category))
86
- return false;
87
85
  if (laterRecords.some((candidate) => hasExactResolutionMarker(candidate, record.id)))
88
86
  return false;
87
+ if (usesLegacySameCategoryFallback(record, laterRecords))
88
+ return false;
89
89
  if (hasResidualRiskDocumentation(record, taskDocs))
90
90
  return false;
91
91
  return true;
@@ -137,7 +137,7 @@ function analyzeTaskEvidenceSemantics(input) {
137
137
  message: 'Task has unresolved failed evidence while marked Done.',
138
138
  evidenceId: record.id,
139
139
  path: `${input.taskDir}/evidence.jsonl`,
140
- expected: `later passed same-category evidence, supersedes:${record.id}, resolves:${record.id}, or explicit residual-risk documentation`,
140
+ expected: `supersedes:${record.id}, resolves:${record.id}, legacy same-category passed evidence, or explicit residual-risk documentation`,
141
141
  actual: 'failed evidence has no resolution signal'
142
142
  });
143
143
  }
@@ -191,8 +191,17 @@ function isRecordOnlyCategory(category) {
191
191
  return exports.RECORD_ONLY_EVIDENCE_CATEGORIES.includes(category);
192
192
  }
193
193
  function hasExactResolutionMarker(record, evidenceId) {
194
+ if (record.outcome !== 'passed' && record.outcome !== 'recorded')
195
+ return false;
194
196
  return record.tags.includes(`supersedes:${evidenceId}`) || record.tags.includes(`resolves:${evidenceId}`);
195
197
  }
198
+ function usesLegacySameCategoryFallback(record, laterRecords) {
199
+ if (record.persistedSchemaVersion !== 'hadara.evidence.v1')
200
+ return false;
201
+ return laterRecords.some((candidate) => candidate.persistedSchemaVersion === 'hadara.evidence.v1' &&
202
+ candidate.outcome === 'passed' &&
203
+ candidate.category === record.category);
204
+ }
196
205
  function hasResidualRiskDocumentation(record, taskDocs) {
197
206
  const content = joinTaskDocs(taskDocs);
198
207
  if (!content)
@@ -25,25 +25,44 @@
25
25
  "evidenceV1": {
26
26
  "type": "object",
27
27
  "additionalProperties": true,
28
- "required": ["schemaVersion", "time", "taskId", "kind", "summary", "result", "visibility"],
28
+ "required": ["schemaVersion", "id", "sourceLine", "idSource", "idStability", "persistedSchemaVersion", "time", "taskId", "kind", "summary", "result", "visibility", "category", "outcome", "tags", "legacy"],
29
29
  "properties": {
30
30
  "schemaVersion": { "const": "hadara.evidence.v1" },
31
+ "id": { "type": "string", "pattern": "^legacy:T-[0-9]{4}:[0-9]+:[a-f0-9]{12}$" },
32
+ "sourceLine": { "type": "integer", "minimum": 1 },
33
+ "idSource": { "const": "line-fallback" },
34
+ "idStability": { "const": "unstable-on-reorder" },
35
+ "persistedSchemaVersion": { "const": "hadara.evidence.v1" },
31
36
  "time": { "type": "string", "minLength": 1 },
32
37
  "taskId": { "type": "string", "pattern": "^T-[0-9]{4}$" },
33
38
  "kind": { "type": "string", "enum": ["test-log", "command-log", "diff-summary", "screenshot", "note"] },
34
39
  "summary": { "type": "string", "minLength": 1 },
35
40
  "result": { "type": "string", "enum": ["passed", "failed", "blocked", "unknown"] },
36
- "visibility": { "type": "string", "enum": ["public", "private"] }
41
+ "visibility": { "type": "string", "enum": ["public", "private"] },
42
+ "category": { "type": "string", "enum": ["validation", "implementation", "release", "security", "policy", "operation", "decision", "handoff", "audit", "note", "observation"] },
43
+ "outcome": { "type": "string", "enum": ["passed", "failed", "blocked", "unknown", "recorded", "not-applicable"] },
44
+ "tags": { "type": "array", "items": { "type": "string" } },
45
+ "legacy": {
46
+ "type": "object",
47
+ "additionalProperties": true,
48
+ "required": ["kind", "result"],
49
+ "properties": {
50
+ "kind": { "type": "string", "enum": ["test-log", "command-log", "diff-summary", "screenshot", "note"] },
51
+ "result": { "type": "string", "enum": ["passed", "failed", "blocked", "unknown"] },
52
+ "evidencePath": { "type": "string", "minLength": 1 }
53
+ }
54
+ }
37
55
  }
38
56
  },
39
57
  "evidenceV2": {
40
58
  "type": "object",
41
59
  "additionalProperties": true,
42
- "required": ["schemaVersion", "id", "fingerprint", "idSource", "idStability", "time", "taskId", "category", "outcome", "visibility", "summary", "artifacts", "tags", "legacy"],
60
+ "required": ["schemaVersion", "id", "sourceLine", "persistedSchemaVersion", "fingerprint", "idSource", "idStability", "time", "taskId", "category", "outcome", "visibility", "summary", "artifacts", "tags", "legacy"],
43
61
  "properties": {
44
62
  "schemaVersion": { "const": "hadara.evidence.v2" },
45
63
  "id": { "type": "string", "minLength": 1 },
46
64
  "sourceLine": { "type": "integer", "minimum": 1 },
65
+ "persistedSchemaVersion": { "const": "hadara.evidence.v2" },
47
66
  "fingerprint": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
48
67
  "idSource": { "const": "persisted" },
49
68
  "idStability": { "const": "durable" },
@@ -475,8 +475,8 @@ exports.HADARA_COMMAND_REGISTRY = [
475
475
  },
476
476
  {
477
477
  id: 'evidence.add-command',
478
- command: 'hadara evidence add-command --task <task-id> --summary <text> [--result <result>] [--idempotency-key <key>] [--json]',
479
- summary: 'Append command-log evidence to a Task Capsule.',
478
+ command: 'hadara evidence add-command --task <task-id> --summary <text> [--result <result>] [--outcome <outcome>] [--category <category>] [--resolves <id>] [--supersedes <id>] [--idempotency-key <key>] [--json]',
479
+ summary: 'Append command-log evidence to a Task Capsule, rejecting incompatible result/outcome metadata.',
480
480
  canonical: true,
481
481
  appearsInDefaultHelp: true,
482
482
  family: 'capsule-lifecycle',
@@ -490,14 +490,17 @@ exports.HADARA_COMMAND_REGISTRY = [
490
490
  status: 'stable',
491
491
  schemaVersion: 'hadara.evidence.collect.v1',
492
492
  docs: ['docs/IMPLEMENTATION_SOP.md'],
493
- examples: [example('Record command evidence', 'hadara evidence add-command --task T-0001 --summary "npm test passed" --result passed --json', 'After meaningful validation.')],
493
+ examples: [
494
+ example('Record command evidence', 'hadara evidence add-command --task T-0001 --summary "npm test passed" --result passed --category validation --json', 'After meaningful validation.')
495
+ ],
494
496
  related: ['evidence.list', 'evidence.lint', 'task.ready'],
497
+ notes: 'The collect response remains `hadara.evidence.collect.v1` with additive v2 metadata. A new add-command report schema id, check-id, and subject fields are deferred candidate scope.',
495
498
  conflictsWith: []
496
499
  },
497
500
  {
498
501
  id: 'evidence.list',
499
502
  command: 'hadara evidence list --task <task-id> [--limit <n>] [--include-private] [--json]',
500
- summary: 'List public evidence index records for a Task Capsule.',
503
+ summary: 'List Task Capsule evidence ids, category/outcome metadata, and sanitized evidence summaries.',
501
504
  canonical: true,
502
505
  appearsInDefaultHelp: false,
503
506
  family: 'capsule-lifecycle',
@@ -511,7 +514,7 @@ exports.HADARA_COMMAND_REGISTRY = [
511
514
  status: 'stable',
512
515
  schemaVersion: 'hadara.evidence.list.v1',
513
516
  docs: ['docs/IMPLEMENTATION_SOP.md'],
514
- examples: [example('List evidence', 'hadara evidence list --task T-0001 --json', 'When auditing a capsule evidence trail.')],
517
+ examples: [example('List evidence ids', 'hadara evidence list --task T-0001', 'Before copying a durable ev: id into --resolves or --supersedes.')],
515
518
  related: ['evidence.add-command', 'evidence.lint'],
516
519
  conflictsWith: []
517
520
  },
@@ -552,9 +555,10 @@ exports.HADARA_COMMAND_REGISTRY = [
552
555
  actor: 'operator',
553
556
  status: 'stable',
554
557
  schemaVersion: 'hadara.evidence.migration_preview.v1',
555
- docs: ['docs/EVIDENCE_SEMANTIC_CONTRACT.md'],
558
+ docs: ['docs/EVIDENCE_SEMANTIC_CONTRACT.md', 'docs/CLI_JSON_CONTRACT.md'],
556
559
  examples: [example('Preview evidence migration', 'hadara evidence migrate --task T-0001 --to v2 --json', 'When older evidence files need migration.')],
557
560
  related: ['evidence.lint', 'protocol.remediate'],
561
+ notes: 'This is an operator-selected JSONL migration surface, not evidence rebuild. 0.3.2 does not register or implement `hadara evidence rebuild`; `EVIDENCE.md` remains a non-canonical human summary.',
558
562
  conflictsWith: []
559
563
  },
560
564
  {
@@ -8,6 +8,7 @@ exports.parseEvidenceIndexFile = parseEvidenceIndexFile;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const redaction_1 = require("../core/redaction");
11
+ const normalizer_1 = require("../evidence/normalizer");
11
12
  const task_capsule_1 = require("../task/task-capsule");
12
13
  const DEFAULT_LIMIT = 50;
13
14
  function createEvidenceListReport(projectRoot, input) {
@@ -73,7 +74,7 @@ function parseEvidenceIndexFile(indexPath, taskId) {
73
74
  });
74
75
  return;
75
76
  }
76
- records.push(normalizeEvidenceIndexRecord(record));
77
+ records.push(normalizeEvidenceIndexRecord(record, index + 1));
77
78
  return;
78
79
  }
79
80
  issues.push({
@@ -132,9 +133,10 @@ function isEvidenceV2IndexRecord(value) {
132
133
  isEvidenceResult(record.legacy.result) &&
133
134
  (record.legacy.evidencePath === undefined || typeof record.legacy.evidencePath === 'string'));
134
135
  }
135
- function normalizeEvidenceIndexRecord(record) {
136
+ function normalizeEvidenceIndexRecord(record, sourceLine) {
136
137
  if (record.schemaVersion === 'hadara.evidence.v2')
137
- return normalizeEvidenceV2IndexRecord(record);
138
+ return normalizeEvidenceV2IndexRecord(record, sourceLine);
139
+ const normalizedRecord = (0, normalizer_1.normalizeEvidenceRecord)(record, { lineNumber: sourceLine });
138
140
  const normalized = {
139
141
  schemaVersion: 'hadara.evidence.v1',
140
142
  time: record.time,
@@ -147,13 +149,29 @@ function normalizeEvidenceIndexRecord(record) {
147
149
  if (record.visibility === 'public' && record.evidencePath) {
148
150
  normalized.evidencePath = record.evidencePath;
149
151
  }
150
- return normalized;
152
+ return {
153
+ ...normalized,
154
+ id: normalizedRecord.id,
155
+ sourceLine,
156
+ idSource: 'line-fallback',
157
+ idStability: 'unstable-on-reorder',
158
+ persistedSchemaVersion: 'hadara.evidence.v1',
159
+ category: normalizedRecord.category,
160
+ outcome: normalizedRecord.outcome,
161
+ tags: normalizedRecord.tags,
162
+ legacy: {
163
+ kind: normalized.kind,
164
+ result: normalized.result,
165
+ ...(normalized.evidencePath ? { evidencePath: normalized.evidencePath } : {})
166
+ }
167
+ };
151
168
  }
152
- function normalizeEvidenceV2IndexRecord(record) {
169
+ function normalizeEvidenceV2IndexRecord(record, sourceLine) {
153
170
  return {
154
171
  schemaVersion: 'hadara.evidence.v2',
155
172
  id: record.id,
156
- ...(record.sourceLine ? { sourceLine: record.sourceLine } : {}),
173
+ sourceLine: record.sourceLine ?? sourceLine,
174
+ persistedSchemaVersion: 'hadara.evidence.v2',
157
175
  fingerprint: record.fingerprint,
158
176
  idSource: 'persisted',
159
177
  idStability: 'durable',
@@ -349,7 +349,7 @@ function createSelectedTaskReadModel(projectRoot, summary, options = {}) {
349
349
  }
350
350
  function createFastSelectedTaskReadModel(projectRoot, summary, options) {
351
351
  const detail = createTaskDocumentReadReportFromSummary(projectRoot, summary, { includePrivate: false });
352
- const evidenceIndex = detail.evidenceIndex ?? [];
352
+ const evidenceIndex = (detail.evidenceIndex ?? []);
353
353
  const evidence = limitEvidenceListReport({
354
354
  schemaVersion: 'hadara.evidence.list.v1',
355
355
  command: 'evidence.list',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadara",
3
- "version": "0.3.1-rc.1",
3
+ "version": "0.3.2-rc.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Portable AI-assisted development workbench for evidence-backed task capsules, handoffs, and release gates.",