hadara 0.4.0-rc.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +19 -19
  2. package/dist/cli/doctor.js +74 -3
  3. package/dist/cli/evidence-json.js +3 -0
  4. package/dist/cli/evidence.js +1 -0
  5. package/dist/cli/release-artifact.js +1 -1
  6. package/dist/cli/task.js +65 -11
  7. package/dist/cli/validation.js +14 -4
  8. package/dist/core/timing.js +12 -0
  9. package/dist/dev/docker-check.js +3 -2
  10. package/dist/evidence/evidence.js +3 -2
  11. package/dist/handoff/handoff-suggestion.js +1 -0
  12. package/dist/handoff/handoff.js +1 -0
  13. package/dist/harness/validate.js +140 -36
  14. package/dist/services/capability-registry.js +6 -5
  15. package/dist/services/ci-gate.js +1 -0
  16. package/dist/services/clean-checkout-smoke.js +4 -2
  17. package/dist/services/dashboard-bootstrap.js +16 -1
  18. package/dist/services/dashboard-task-detail.js +1 -0
  19. package/dist/services/package-recycle.js +5 -2
  20. package/dist/services/package-smoke.js +7 -2
  21. package/dist/services/proof-status.js +2 -0
  22. package/dist/services/protocol-consistency.js +2 -1
  23. package/dist/services/protocol-migration.js +1 -0
  24. package/dist/services/release-artifact.js +3 -2
  25. package/dist/services/release-closeout.js +1 -0
  26. package/dist/services/release-diagnostics.js +5 -4
  27. package/dist/services/release-dry-run.js +3 -2
  28. package/dist/services/task-read-model.js +4 -0
  29. package/dist/services/task-workbench.js +27 -11
  30. package/dist/services/validation-run.js +11 -3
  31. package/dist/services/write-preflight.js +2 -13
  32. package/dist/task/acceptance.js +27 -5
  33. package/dist/task/authoring-guidance.js +6 -4
  34. package/dist/task/task-capsule.js +20 -9
  35. package/dist/task/task-create.js +1 -0
  36. package/dist/task/task-templates.js +1 -1
  37. package/dist/task/task-upgrade-scaffold.js +8 -6
  38. package/dist/tui/read-model.js +2 -0
  39. package/package.json +1 -1
@@ -26,9 +26,11 @@ const SOURCE_DOCUMENT_ROLE_TOKENS = new Set(['implementation-source', 'reference
26
26
  const SOURCE_DOCUMENT_AUTHORITY_TOKENS = new Set(['exploratory', 'proposed', 'approved', 'normative', 'implementation-source', 'reference-only', 'historical']);
27
27
  const SOURCE_DOCUMENT_STATUS_TOKENS = new Set(['draft', 'review', 'approved', 'implementing', 'implemented', 'superseded', 'drift-risk', 'archived']);
28
28
  const ACCEPTANCE_REQUIRED_TOKENS = new Set(['Yes', 'No']);
29
- const ACCEPTANCE_STATUS_TOKENS = new Set(['Pending', 'Met', 'Not Met', 'Blocked', 'Not Applicable']);
29
+ const ACCEPTANCE_DECISION_TOKENS = new Set(['Must', 'Optional', 'Follow-up', 'Accepted Risk', 'Not Applicable', 'Superseded']);
30
+ const ACCEPTANCE_STATUS_TOKENS = new Set(['Pending', 'In Progress', 'Met', 'Not Met', 'Blocked', 'Partial', 'Deferred', 'Follow-up Created', 'Accepted Risk', 'Not Applicable', 'Superseded']);
30
31
  const ACCEPTANCE_DISPOSITION_TOKENS = new Set(['Required', 'Optional', 'Deferred', 'Accepted Risk', 'Not Applicable', 'Superseded']);
31
32
  const ACCEPTANCE_DISPOSITIONS_REQUIRING_REFERENCE = new Set(['Deferred', 'Accepted Risk', 'Not Applicable', 'Superseded']);
33
+ const ACCEPTANCE_DECISIONS_REQUIRING_REFERENCE = new Set(['Follow-up', 'Accepted Risk', 'Not Applicable', 'Superseded']);
32
34
  const VALIDATION_REQUIRED_TOKENS = new Set(['Yes', 'No']);
33
35
  const VALIDATION_RESULT_TOKENS = new Set(['Not Run', 'Passed', 'Failed', 'Blocked', 'Skipped', 'Not Applicable']);
34
36
  const RISK_KIND_TOKENS = new Set(['Risk', 'Follow-up', 'Question']);
@@ -49,6 +51,7 @@ function validateTaskCapsule(projectRoot, taskId, options = {}) {
49
51
  schemaVersion: 'hadara.harness.validate.v1',
50
52
  command: 'harness.validate',
51
53
  ok: false,
54
+ taskId,
52
55
  level,
53
56
  task: { id: taskId, title: '', capsule: '' },
54
57
  checkedFiles: [],
@@ -93,6 +96,7 @@ function validateTaskCapsule(projectRoot, taskId, options = {}) {
93
96
  schemaVersion: 'hadara.harness.validate.v1',
94
97
  command: 'harness.validate',
95
98
  ok: !issues.some((issue) => issue.severity === 'error'),
99
+ taskId: task.id,
96
100
  level,
97
101
  task: {
98
102
  id: task.id,
@@ -112,24 +116,11 @@ function validateTaskMarkdown(projectRoot, task, issues, level) {
112
116
  return;
113
117
  const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, taskPath));
114
118
  const content = node_fs_1.default.readFileSync(taskPath, 'utf8');
115
- for (const heading of ['## Identity', '## Source Documents', '## Goal', '## Plan', '## Acceptance', '## Validation', '## Change Summary', '## Risks / Follow-ups']) {
116
- if (!content.includes(heading)) {
117
- issues.push({
118
- severity: 'error',
119
- code: 'TASK_SECTION_MISSING',
120
- message: `TASK.md is missing required section: ${heading}`,
121
- path: relativePath,
122
- heading,
123
- fixHint: `Add the ${heading} section to TASK.md and fill it with task-specific content.`,
124
- remediationHint: {
125
- path: relativePath,
126
- heading,
127
- requiredChange: `Add the missing ${heading} section to TASK.md.`,
128
- blocking: true
129
- }
130
- });
131
- }
119
+ for (const heading of ['## Identity', '## Goal', '## Plan', '## Acceptance', '## Validation', '## Risks / Follow-ups']) {
120
+ requireTaskSection(content, relativePath, issues, heading);
132
121
  }
122
+ requireAnyTaskSection(content, relativePath, issues, ['## Inputs / Constraints', '## Source Documents']);
123
+ requireAnyTaskSection(content, relativePath, issues, ['## Changes', '## Change Summary']);
133
124
  validateTaskIdentityTable(content, relativePath, issues);
134
125
  validateTaskSourceDocumentsTable(projectRoot, content, relativePath, issues, level);
135
126
  validateTaskPlanTable(content, relativePath, issues);
@@ -137,6 +128,7 @@ function validateTaskMarkdown(projectRoot, task, issues, level) {
137
128
  validateTaskValidationTable(content, relativePath, issues);
138
129
  validateTaskChangeSummaryTable(content, relativePath, issues);
139
130
  validateTaskRisksTable(content, relativePath, issues);
131
+ validateTaskHistoryTable(content, relativePath, issues);
140
132
  validateTaskCloseStateBoundaries(content, relativePath, issues);
141
133
  }
142
134
  function validateTaskIdentityTable(content, relativePath, issues) {
@@ -155,19 +147,29 @@ function validateTaskIdentityTable(content, relativePath, issues) {
155
147
  }
156
148
  }
157
149
  function validateTaskSourceDocumentsTable(projectRoot, content, relativePath, issues, level) {
158
- const heading = '## Source Documents';
150
+ const heading = sectionHeading(content, ['## Inputs / Constraints', '## Source Documents']);
159
151
  const table = sectionTable(content, heading);
160
- if (!requireTableHeader(table.rows, ['Path', 'Role', 'Authority', 'Status', 'Source Hash', 'Notes'], relativePath, heading, issues))
152
+ const recognizedHeader = requireAnyTableHeader(table.rows, [
153
+ ['Source', 'Role', 'State', 'Notes'],
154
+ ['Path / Source', 'Type', 'Authority', 'State', 'Notes', 'Hash'],
155
+ ['Path', 'Role', 'Authority', 'Status', 'Source Hash', 'Notes']
156
+ ], relativePath, heading, issues);
157
+ if (!recognizedHeader)
161
158
  return;
159
+ const hasHashColumn = table.header.includes('Hash') || table.header.includes('Source Hash');
160
+ const hasAuthorityColumn = table.header.includes('Authority');
162
161
  for (const row of table.dataRows) {
163
- const pathCell = tableCell(row, table.header, 'Path');
162
+ const pathCell = tableCellAny(row, table.header, ['Source', 'Path / Source', 'Path']);
164
163
  const sourcePath = normalizeSourceDocumentPathCell(pathCell);
165
164
  if (!sourcePath || /^TBD$/i.test(sourcePath))
166
165
  continue;
167
- checkToken(tableCell(row, table.header, 'Role'), SOURCE_DOCUMENT_ROLE_TOKENS, 'TASK_SOURCE_DOCUMENT_ROLE_INVALID_TOKEN', relativePath, heading, issues);
168
- checkToken(tableCell(row, table.header, 'Authority'), SOURCE_DOCUMENT_AUTHORITY_TOKENS, 'TASK_SOURCE_DOCUMENT_AUTHORITY_INVALID_TOKEN', relativePath, heading, issues);
169
- checkToken(tableCell(row, table.header, 'Status'), SOURCE_DOCUMENT_STATUS_TOKENS, 'TASK_SOURCE_DOCUMENT_STATUS_INVALID_TOKEN', relativePath, heading, issues);
170
- const hash = tableCell(row, table.header, 'Source Hash');
166
+ checkToken(tableCellAny(row, table.header, ['Type', 'Role']), SOURCE_DOCUMENT_ROLE_TOKENS, 'TASK_SOURCE_DOCUMENT_ROLE_INVALID_TOKEN', relativePath, heading, issues);
167
+ if (hasAuthorityColumn)
168
+ checkToken(tableCell(row, table.header, 'Authority'), SOURCE_DOCUMENT_AUTHORITY_TOKENS, 'TASK_SOURCE_DOCUMENT_AUTHORITY_INVALID_TOKEN', relativePath, heading, issues);
169
+ checkToken(tableCellAny(row, table.header, ['State', 'Status']), SOURCE_DOCUMENT_STATUS_TOKENS, 'TASK_SOURCE_DOCUMENT_STATUS_INVALID_TOKEN', relativePath, heading, issues);
170
+ if (!hasHashColumn)
171
+ continue;
172
+ const hash = tableCellAny(row, table.header, ['Hash', 'Source Hash']);
171
173
  if (!isSourceHashCell(hash)) {
172
174
  issues.push(taskTableIssue('TASK_SOURCE_DOCUMENT_MISSING_HASH', `Source document "${sourcePath}" must use Source Hash "TBD" or "sha256:<hex>".`, relativePath, heading));
173
175
  continue;
@@ -213,7 +215,7 @@ function hashFile(filePath) {
213
215
  function validateTaskPlanTable(content, relativePath, issues) {
214
216
  const heading = '## Plan';
215
217
  const table = sectionTable(content, heading);
216
- if (!requireTableHeader(table.rows, ['Step', 'Action', 'Status', 'Evidence'], relativePath, heading, issues))
218
+ if (!requireAnyTableHeader(table.rows, [['Step', 'Action', 'Status'], ['Step', 'Action', 'Status', 'Evidence']], relativePath, heading, issues))
217
219
  return;
218
220
  for (const row of table.dataRows) {
219
221
  if (!row.some(Boolean))
@@ -224,7 +226,11 @@ function validateTaskPlanTable(content, relativePath, issues) {
224
226
  function validateTaskAcceptanceTable(content, relativePath, issues) {
225
227
  const heading = '## Acceptance';
226
228
  const table = sectionTable(content, heading);
227
- if (!requireTableHeader(table.rows, ['ID', 'Criterion', 'Required', 'Status', 'Evidence', 'Disposition', 'Reference'], relativePath, heading, issues))
229
+ if (!requireAnyTableHeader(table.rows, [
230
+ ['ID', 'Criterion', 'State', 'Evidence', 'Reference'],
231
+ ['ID', 'Criterion', 'Decision', 'State', 'Evidence', 'Reference'],
232
+ ['ID', 'Criterion', 'Required', 'Status', 'Evidence', 'Disposition', 'Reference']
233
+ ], relativePath, heading, issues))
228
234
  return;
229
235
  for (const row of table.dataRows) {
230
236
  const id = tableCell(row, table.header, 'ID');
@@ -233,6 +239,19 @@ function validateTaskAcceptanceTable(content, relativePath, issues) {
233
239
  if (!/^AC-\d+$/.test(id)) {
234
240
  issues.push(taskTableIssue('ACCEPTANCE_ID_INVALID_TOKEN', `Acceptance ID must use AC-1, AC-2, ...; got "${id}".`, relativePath, heading));
235
241
  }
242
+ const hasStateColumn = table.header.includes('State');
243
+ const hasDecisionColumn = table.header.includes('Decision');
244
+ const decision = tableCell(row, table.header, 'Decision');
245
+ if (hasStateColumn) {
246
+ if (hasDecisionColumn && decision) {
247
+ checkToken(decision, ACCEPTANCE_DECISION_TOKENS, 'ACCEPTANCE_DECISION_INVALID_TOKEN', relativePath, heading, issues);
248
+ if (ACCEPTANCE_DECISIONS_REQUIRING_REFERENCE.has(decision) && isMissingReference(tableCell(row, table.header, 'Reference'))) {
249
+ issues.push(taskTableIssue('ACCEPTANCE_DECISION_REFERENCE_MISSING', `${id} decision "${decision}" requires a concrete reference.`, relativePath, heading));
250
+ }
251
+ }
252
+ checkToken(tableCell(row, table.header, 'State'), ACCEPTANCE_STATUS_TOKENS, 'ACCEPTANCE_STATUS_INVALID_TOKEN', relativePath, heading, issues);
253
+ continue;
254
+ }
236
255
  checkToken(tableCell(row, table.header, 'Required'), ACCEPTANCE_REQUIRED_TOKENS, 'ACCEPTANCE_REQUIRED_INVALID_TOKEN', relativePath, heading, issues);
237
256
  checkToken(tableCell(row, table.header, 'Status'), ACCEPTANCE_STATUS_TOKENS, 'ACCEPTANCE_STATUS_INVALID_TOKEN', relativePath, heading, issues);
238
257
  const disposition = tableCell(row, table.header, 'Disposition');
@@ -245,20 +264,34 @@ function validateTaskAcceptanceTable(content, relativePath, issues) {
245
264
  function validateTaskValidationTable(content, relativePath, issues) {
246
265
  const heading = '## Validation';
247
266
  const table = sectionTable(content, heading);
248
- if (!requireTableHeader(table.rows, ['Check', 'Command / Method', 'Required', 'Latest Result', 'Evidence'], relativePath, heading, issues))
267
+ if (!requireAnyTableHeader(table.rows, [
268
+ ['Check', 'Gate', 'Result', 'Evidence'],
269
+ ['Check', 'Command / Method', 'Required', 'Latest Result', 'Evidence']
270
+ ], relativePath, heading, issues))
249
271
  return;
250
272
  for (const row of table.dataRows) {
251
273
  if (!row.some(Boolean))
252
274
  continue;
253
- checkToken(tableCell(row, table.header, 'Required'), VALIDATION_REQUIRED_TOKENS, 'VALIDATION_REQUIRED_INVALID_TOKEN', relativePath, heading, issues);
254
- checkToken(tableCell(row, table.header, 'Latest Result'), VALIDATION_RESULT_TOKENS, 'VALIDATION_RESULT_INVALID_TOKEN', relativePath, heading, issues);
275
+ checkToken(tableCellAny(row, table.header, ['Gate', 'Required']), VALIDATION_REQUIRED_TOKENS, 'VALIDATION_REQUIRED_INVALID_TOKEN', relativePath, heading, issues);
276
+ checkToken(tableCellAny(row, table.header, ['Result', 'Latest Result']), VALIDATION_RESULT_TOKENS, 'VALIDATION_RESULT_INVALID_TOKEN', relativePath, heading, issues);
255
277
  }
256
278
  }
257
279
  function validateTaskChangeSummaryTable(content, relativePath, issues) {
258
- const heading = '## Change Summary';
280
+ const heading = sectionHeading(content, ['## Changes', '## Change Summary']);
259
281
  const table = sectionTable(content, heading);
260
282
  if (!requireChangeSummaryHeader(table.rows, relativePath, heading, issues))
261
283
  return;
284
+ if (table.header[0] === 'Area') {
285
+ for (const row of table.dataRows) {
286
+ if (!row.some(Boolean))
287
+ continue;
288
+ const area = tableCell(row, table.header, 'Area');
289
+ if (!area) {
290
+ issues.push(taskTableIssue('CHANGE_SUMMARY_AREA_MISSING', 'Changes rows require an Area value.', relativePath, heading, 'module:task status'));
291
+ }
292
+ }
293
+ return;
294
+ }
262
295
  if (table.header[1] === 'Area') {
263
296
  for (const row of table.dataRows) {
264
297
  if (!row.some(Boolean))
@@ -284,33 +317,88 @@ function validateTaskChangeSummaryTable(content, relativePath, issues) {
284
317
  }
285
318
  function requireChangeSummaryHeader(rows, relativePath, heading, issues) {
286
319
  const actual = rows[0] ?? [];
320
+ const summaryHeader = ['Area', 'Summary'];
321
+ const summaryWithEvidenceHeader = ['Area', 'Summary', 'Evidence'];
287
322
  const areaHeader = ['Path', 'Area', 'Change', 'Reason', 'Evidence'];
288
323
  const legacyLinesHeader = ['Path', 'Lines', 'Change', 'Reason', 'Evidence'];
289
- if (areaHeader.every((cell, index) => actual[index] === cell) || legacyLinesHeader.every((cell, index) => actual[index] === cell))
324
+ if (summaryHeader.every((cell, index) => actual[index] === cell) ||
325
+ summaryWithEvidenceHeader.every((cell, index) => actual[index] === cell) ||
326
+ areaHeader.every((cell, index) => actual[index] === cell) ||
327
+ legacyLinesHeader.every((cell, index) => actual[index] === cell))
290
328
  return true;
291
- issues.push(taskTableIssue('TASK_TABLE_SCHEMA_INVALID', `${heading} table header must be: | ${areaHeader.join(' | ')} |`, relativePath, heading, `| ${areaHeader.join(' | ')} |`));
329
+ issues.push(taskTableIssue('TASK_TABLE_SCHEMA_INVALID', `${heading} table header must be: | ${summaryHeader.join(' | ')} |`, relativePath, heading, `| ${summaryHeader.join(' | ')} |`));
292
330
  return false;
293
331
  }
294
332
  function validateTaskRisksTable(content, relativePath, issues) {
295
333
  const heading = '## Risks / Follow-ups';
296
334
  const table = sectionTable(content, heading);
297
- if (!requireTableHeader(table.rows, ['ID', 'Kind', 'Summary', 'State', 'Reference'], relativePath, heading, issues))
335
+ if (!requireAnyTableHeader(table.rows, [
336
+ ['ID', 'Type', 'Summary', 'State', 'Link'],
337
+ ['ID', 'Kind', 'Summary', 'State', 'Reference']
338
+ ], relativePath, heading, issues))
298
339
  return;
299
340
  for (const row of table.dataRows) {
300
341
  if (!row.some(Boolean))
301
342
  continue;
302
- checkToken(tableCell(row, table.header, 'Kind'), RISK_KIND_TOKENS, 'TASK_RISK_KIND_INVALID_TOKEN', relativePath, heading, issues);
343
+ checkToken(tableCellAny(row, table.header, ['Type', 'Kind']), RISK_KIND_TOKENS, 'TASK_RISK_KIND_INVALID_TOKEN', relativePath, heading, issues);
303
344
  checkToken(tableCell(row, table.header, 'State'), RISK_STATE_TOKENS, 'TASK_RISK_STATE_INVALID_TOKEN', relativePath, heading, issues);
304
345
  }
305
346
  }
347
+ function validateTaskHistoryTable(content, relativePath, issues) {
348
+ if (!content.includes('## History'))
349
+ return;
350
+ const heading = '## History';
351
+ const table = sectionTable(content, heading);
352
+ requireTableHeader(table.rows, ['Date', 'State', 'Note'], relativePath, heading, issues);
353
+ }
306
354
  function validateTaskCloseStateBoundaries(content, relativePath, issues) {
307
355
  if (/^\|\s*CloseState\s*\|/im.test(content) || /^\|\s*Close State\s*\|/im.test(content)) {
308
356
  issues.push(taskTableIssue('TASK_CLOSE_STATE_PERSISTED_IN_TASK', 'TASK.md must not persist derived CloseState values.', relativePath, 'TASK.md'));
309
357
  }
310
- if (/\n##\s*Close Proof\b/i.test(content) || /\baudit-close\b/i.test((0, markdown_table_1.readMarkdownSection)(content, '## Change Summary'))) {
358
+ if (/\n##\s*Close Proof\b/i.test(content) || /\baudit-close\b/i.test((0, markdown_table_1.readMarkdownSection)(content, sectionHeading(content, ['## Changes', '## Change Summary'])))) {
311
359
  issues.push(taskTableIssue('TASK_CLOSE_PROOF_IN_CLOSE_SOURCE', 'TASK.md must not include close proof or audit-close result tables.', relativePath, 'TASK.md'));
312
360
  }
313
361
  }
362
+ function requireTaskSection(content, relativePath, issues, heading) {
363
+ if (content.includes(heading))
364
+ return;
365
+ issues.push({
366
+ severity: 'error',
367
+ code: 'TASK_SECTION_MISSING',
368
+ message: `TASK.md is missing required section: ${heading}`,
369
+ path: relativePath,
370
+ heading,
371
+ fixHint: `Add the ${heading} section to TASK.md and fill it with task-specific content.`,
372
+ remediationHint: {
373
+ path: relativePath,
374
+ heading,
375
+ requiredChange: `Add the missing ${heading} section to TASK.md.`,
376
+ blocking: true
377
+ }
378
+ });
379
+ }
380
+ function requireAnyTaskSection(content, relativePath, issues, headings) {
381
+ if (headings.some((heading) => content.includes(heading)))
382
+ return;
383
+ const heading = headings[0];
384
+ issues.push({
385
+ severity: 'error',
386
+ code: 'TASK_SECTION_MISSING',
387
+ message: `TASK.md is missing required section: ${headings.join(' or ')}`,
388
+ path: relativePath,
389
+ heading,
390
+ fixHint: `Add the ${heading} section to TASK.md and fill it with task-specific content.`,
391
+ remediationHint: {
392
+ path: relativePath,
393
+ heading,
394
+ requiredChange: `Add one of these sections to TASK.md: ${headings.join(', ')}.`,
395
+ blocking: true
396
+ }
397
+ });
398
+ }
399
+ function sectionHeading(content, headings) {
400
+ return headings.find((heading) => content.includes(heading)) ?? headings[0];
401
+ }
314
402
  function sectionRows(content, heading) {
315
403
  return (0, markdown_table_1.parseMarkdownRows)((0, markdown_table_1.readMarkdownSection)(content, heading));
316
404
  }
@@ -329,10 +417,26 @@ function requireTableHeader(rows, expected, relativePath, heading, issues) {
329
417
  issues.push(taskTableIssue('TASK_TABLE_SCHEMA_INVALID', `${heading} table header must be: | ${expected.join(' | ')} |`, relativePath, heading, `| ${expected.join(' | ')} |`));
330
418
  return false;
331
419
  }
420
+ function requireAnyTableHeader(rows, expectedHeaders, relativePath, heading, issues) {
421
+ const actual = rows[0] ?? [];
422
+ if (expectedHeaders.some((expected) => expected.every((cell, index) => actual[index] === cell)))
423
+ return true;
424
+ const preferred = expectedHeaders[0];
425
+ issues.push(taskTableIssue('TASK_TABLE_SCHEMA_INVALID', `${heading} table header must be: | ${preferred.join(' | ')} |`, relativePath, heading, `| ${preferred.join(' | ')} |`));
426
+ return false;
427
+ }
332
428
  function tableCell(row, header, name) {
333
429
  const index = header.findIndex((cell) => cell === name);
334
430
  return index >= 0 ? (row[index] ?? '').trim() : '';
335
431
  }
432
+ function tableCellAny(row, header, names) {
433
+ for (const name of names) {
434
+ const value = tableCell(row, header, name);
435
+ if (value)
436
+ return value;
437
+ }
438
+ return '';
439
+ }
336
440
  function checkToken(value, allowed, code, relativePath, heading, issues) {
337
441
  if (allowed.has(value))
338
442
  return;
@@ -300,8 +300,8 @@ exports.HADARA_COMMAND_REGISTRY = [
300
300
  },
301
301
  {
302
302
  id: 'task.status',
303
- command: 'hadara task status [--task <task-id>] [--detail fast|full] [--json]',
304
- summary: 'Read the phase-aware task cockpit: next-work selection without --task, fast selected-capsule loop guidance with --task, or explicit full diagnostics with --detail full.',
303
+ command: 'hadara task status [--task <task-id>] [--detail fast|full] [--json|--summary-json]',
304
+ summary: 'Read the phase-aware task cockpit: next-work selection without --task, fast selected-capsule loop guidance with --task, compact selected-capsule summary with --summary-json, or explicit full diagnostics with --detail full.',
305
305
  canonical: true,
306
306
  appearsInDefaultHelp: true,
307
307
  family: 'capsule-lifecycle',
@@ -318,6 +318,7 @@ exports.HADARA_COMMAND_REGISTRY = [
318
318
  examples: [
319
319
  example('Select next work', 'hadara task status --json', 'At session start or after a capsule closes.'),
320
320
  example('Inspect capsule status', 'hadara task status --task T-0001 --json', 'At loop boundaries for a selected capsule.'),
321
+ example('Inspect compact capsule status', 'hadara task status --task T-0001 --summary-json', 'When shell automation or humans only need phase, readiness, counts, and next action.'),
321
322
  example('Inspect full diagnostics', 'hadara task status --task T-0001 --detail full --json', 'When explicit close/protocol diagnostics are needed without finalize planning.')
322
323
  ],
323
324
  related: ['task.next', 'evidence.list', 'proof.status'],
@@ -1868,9 +1869,9 @@ exports.HADARA_COMMAND_REGISTRY = [
1868
1869
  testFiles: ['tests/unit/package-recycle.test.ts'],
1869
1870
  docs: ['docs/RELEASE_READINESS.md', 'docs/CLI_JSON_CONTRACT.md'],
1870
1871
  examples: [
1871
- example('Preview installed-package recycle', 'hadara package recycle --package hadara@latest --expected-version 0.3.3 --json', 'Before running registry-backed consumer install validation.'),
1872
- example('Execute installed-package recycle', 'hadara package recycle --execute --package hadara@latest --expected-version 0.3.3 --task T-XXXX --attach-evidence --json', 'After an npm publish when verifying consumer install paths.'),
1873
- example('Execute recycle with graph diagnostics', 'hadara package recycle --execute --package hadara@latest --expected-version 0.3.3 --include-graph --json', 'When intentionally running the broader context graph smoke.')
1872
+ example('Preview installed-package recycle', 'hadara package recycle --package hadara@latest --expected-version 0.4.0 --json', 'Before running registry-backed consumer install validation.'),
1873
+ example('Execute installed-package recycle', 'hadara package recycle --execute --package hadara@latest --expected-version 0.4.0 --task T-XXXX --attach-evidence --json', 'After an npm publish when verifying consumer install paths.'),
1874
+ example('Execute recycle with graph diagnostics', 'hadara package recycle --execute --package hadara@latest --expected-version 0.4.0 --include-graph --json', 'When intentionally running the broader context graph smoke.')
1874
1875
  ],
1875
1876
  related: ['package.smoke', 'release.closeout', 'release.publish'],
1876
1877
  conflictsWith: [],
@@ -83,6 +83,7 @@ function createCiGateReport(projectRoot, mode, options = {}) {
83
83
  schemaVersion: 'hadara.ci.gate.v1',
84
84
  command: 'ci.gate',
85
85
  ok: mode === 'advisory' ? true : blockers.length === 0,
86
+ ...(options.taskId ? { taskId: options.taskId } : {}),
86
87
  mode,
87
88
  scope: { ...(options.taskId ? { taskId: options.taskId } : {}), taskCount: tasks.length, allowEmpty },
88
89
  stateConsistency,
@@ -9,6 +9,7 @@ const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_child_process_1 = require("node:child_process");
11
11
  const schema_1 = require("../core/schema");
12
+ const timing_1 = require("../core/timing");
12
13
  const smoke_evidence_1 = require("./smoke-evidence");
13
14
  function createCleanCheckoutSmokeReport(options) {
14
15
  const issues = [];
@@ -153,6 +154,7 @@ function createCleanCheckoutSmokeReport(options) {
153
154
  schemaVersion: 'hadara.cleanCheckoutSmoke.v1',
154
155
  command: 'smoke.cleanCheckout',
155
156
  ok: issues.every((issue) => issue.severity !== 'error') && steps.every((step) => step.status !== 'failed'),
157
+ ...(options.taskId ? { taskId: options.taskId } : {}),
156
158
  mode: 'execute',
157
159
  execution,
158
160
  workspace: {
@@ -328,7 +330,7 @@ function cleanupWorkspace(workspace, keepTemp) {
328
330
  }
329
331
  }
330
332
  function runCommand(command, args, options) {
331
- const started = Date.now();
333
+ const timer = (0, timing_1.startMonotonicTimer)();
332
334
  const result = (0, node_child_process_1.spawnSync)(command, args, {
333
335
  cwd: options.cwd,
334
336
  env: options.env ?? process.env,
@@ -341,7 +343,7 @@ function runCommand(command, args, options) {
341
343
  signal: result.signal,
342
344
  stdout: result.stdout ?? '',
343
345
  stderr: result.stderr ?? '',
344
- elapsedMs: Date.now() - started,
346
+ elapsedMs: timer.elapsedMs(),
345
347
  timedOut: result.error?.name === 'TimeoutError' || result.signal === 'SIGTERM'
346
348
  };
347
349
  }
@@ -21,7 +21,7 @@ function createDashboardBootstrapReport(projectRoot, input = {}, now = new Date(
21
21
  ];
22
22
  const selectedTaskId = input.selectedTaskId?.trim();
23
23
  const selectedTask = selectedTaskId ? createSelectedTaskSummary(projectRoot, selectedTaskId, now, issues) : null;
24
- return {
24
+ const report = {
25
25
  schemaVersion: 'hadara.dashboard.bootstrap.v1',
26
26
  command: 'dashboard.bootstrap',
27
27
  ok: !issues.some((issue) => issue.severity === 'error'),
@@ -65,6 +65,7 @@ function createDashboardBootstrapReport(projectRoot, input = {}, now = new Date(
65
65
  selectedTask,
66
66
  issues
67
67
  };
68
+ return redactArtifactPaths(report);
68
69
  }
69
70
  function createSelectedTaskSummary(projectRoot, taskId, now, aggregateIssues) {
70
71
  const workbench = (0, task_workbench_1.createTaskWorkbenchReport)(projectRoot, taskId, now, { detail: 'fast' });
@@ -136,3 +137,17 @@ function proofStatusFrom(semanticIssueCodes, substantivePositive) {
136
137
  return 'sufficient';
137
138
  return 'unknown';
138
139
  }
140
+ function redactArtifactPaths(value) {
141
+ if (typeof value === 'string') {
142
+ return value.replace(/\b\S*artifacts\/\S*/g, '[artifact-path-redacted]');
143
+ }
144
+ if (Array.isArray(value))
145
+ return value.map((item) => redactArtifactPaths(item));
146
+ if (value && typeof value === 'object') {
147
+ const next = {};
148
+ for (const [key, child] of Object.entries(value))
149
+ next[key] = redactArtifactPaths(child);
150
+ return next;
151
+ }
152
+ return value;
153
+ }
@@ -77,6 +77,7 @@ function createFastTaskWorkbenchReport(projectRoot, taskId, evidenceLint, eviden
77
77
  schemaVersion: 'hadara.task.workbench.v1',
78
78
  command: 'task.status',
79
79
  ok: task.present && blockers === 0,
80
+ taskId,
80
81
  generatedAt,
81
82
  projectRoot,
82
83
  task: {
@@ -11,6 +11,7 @@ const node_os_1 = __importDefault(require("node:os"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
12
  const node_child_process_1 = require("node:child_process");
13
13
  const schema_1 = require("../core/schema");
14
+ const timing_1 = require("../core/timing");
14
15
  const smoke_evidence_1 = require("./smoke-evidence");
15
16
  const DEFAULT_PACKAGE_SPECIFIER = 'hadara@latest';
16
17
  function createPackageRecycleReport(options) {
@@ -24,6 +25,7 @@ function createPackageRecycleDryRunReport(options) {
24
25
  schemaVersion: 'hadara.packageRecycle.v1',
25
26
  command: 'package.recycle',
26
27
  ok: issues.every((issue) => issue.severity !== 'error'),
28
+ ...(options.taskId ? { taskId: options.taskId } : {}),
27
29
  mode: 'dry-run',
28
30
  readOnly: true,
29
31
  package: packageInfo,
@@ -194,6 +196,7 @@ function createPackageRecycleExecuteReport(options) {
194
196
  schemaVersion: 'hadara.packageRecycle.v1',
195
197
  command: 'package.recycle',
196
198
  ok: issues.every((issue) => issue.severity !== 'error') && steps.every((step) => step.status !== 'failed'),
199
+ ...(options.taskId ? { taskId: options.taskId } : {}),
197
200
  mode: 'execute',
198
201
  readOnly: false,
199
202
  package: packageInfo,
@@ -661,7 +664,7 @@ function failStep(step, summary) {
661
664
  step.summary = summary;
662
665
  }
663
666
  function runCommand(command, args, options) {
664
- const start = Date.now();
667
+ const timer = (0, timing_1.startMonotonicTimer)();
665
668
  const result = (0, node_child_process_1.spawnSync)(command, args, {
666
669
  cwd: options.cwd,
667
670
  env: options.env ?? process.env,
@@ -674,7 +677,7 @@ function runCommand(command, args, options) {
674
677
  signal: result.signal,
675
678
  stdout: result.stdout ?? '',
676
679
  stderr: result.stderr ?? '',
677
- elapsedMs: Date.now() - start,
680
+ elapsedMs: timer.elapsedMs(),
678
681
  timedOut: result.error?.name === 'Error' && /timed out/i.test(result.error.message)
679
682
  };
680
683
  }
@@ -10,6 +10,7 @@ const node_os_1 = __importDefault(require("node:os"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
11
  const node_child_process_1 = require("node:child_process");
12
12
  const schema_1 = require("../core/schema");
13
+ const timing_1 = require("../core/timing");
13
14
  const smoke_evidence_1 = require("./smoke-evidence");
14
15
  const release_targets_1 = require("./release-targets");
15
16
  function createPackageSmokeDryRunReport(options) {
@@ -35,6 +36,7 @@ function createPackageSmokeDryRunReport(options) {
35
36
  schemaVersion: 'hadara.packageSmoke.v1',
36
37
  command: 'package.smoke',
37
38
  ok: false,
39
+ ...(options.taskId ? { taskId: options.taskId } : {}),
38
40
  mode: 'dry-run',
39
41
  readOnly: true,
40
42
  provider: {
@@ -295,6 +297,7 @@ function createPackageSmokeLocalReport(options) {
295
297
  schemaVersion: 'hadara.packageSmoke.v1',
296
298
  command: 'package.smoke',
297
299
  ok: issues.every((issue) => issue.severity !== 'error') && steps.every((step) => step.status !== 'failed'),
300
+ ...(options.taskId ? { taskId: options.taskId } : {}),
298
301
  mode: 'local',
299
302
  readOnly: false,
300
303
  provider: {
@@ -365,6 +368,7 @@ function createPythonPackageSmokeDryRunReport(options) {
365
368
  schemaVersion: 'hadara.packageSmoke.v1',
366
369
  command: 'package.smoke',
367
370
  ok: issues.every((issue) => issue.severity !== 'error'),
371
+ ...(options.taskId ? { taskId: options.taskId } : {}),
368
372
  mode: 'dry-run',
369
373
  readOnly: true,
370
374
  provider: {
@@ -627,6 +631,7 @@ function createPythonPackageSmokeLocalReport(options) {
627
631
  schemaVersion: 'hadara.packageSmoke.v1',
628
632
  command: 'package.smoke',
629
633
  ok: issues.every((issue) => issue.severity !== 'error') && steps.every((step) => step.status !== 'failed'),
634
+ ...(options.taskId ? { taskId: options.taskId } : {}),
630
635
  mode: 'local',
631
636
  readOnly: false,
632
637
  provider: {
@@ -981,7 +986,7 @@ function cleanupWorkspace(workspace, keepTemp) {
981
986
  }
982
987
  }
983
988
  function runCommand(command, args, options) {
984
- const started = Date.now();
989
+ const timer = (0, timing_1.startMonotonicTimer)();
985
990
  const result = (0, node_child_process_1.spawnSync)(command, args, {
986
991
  cwd: options.cwd,
987
992
  env: options.env ?? process.env,
@@ -994,7 +999,7 @@ function runCommand(command, args, options) {
994
999
  signal: result.signal,
995
1000
  stdout: result.stdout ?? '',
996
1001
  stderr: result.stderr ?? '',
997
- elapsedMs: Date.now() - started,
1002
+ elapsedMs: timer.elapsedMs(),
998
1003
  timedOut: result.error?.name === 'TimeoutError' || result.signal === 'SIGTERM'
999
1004
  };
1000
1005
  }
@@ -17,6 +17,7 @@ function createProofStatusReport(projectRoot, taskId, mode = 'status') {
17
17
  schemaVersion: mode === 'explain' ? 'hadara.proof.explain.v1' : 'hadara.proof.status.v1',
18
18
  command: mode === 'explain' ? 'proof.explain' : 'proof.status',
19
19
  ok: false,
20
+ taskId,
20
21
  target: { kind: 'task', taskId },
21
22
  claim: 'task-readiness',
22
23
  verdict: 'unknown',
@@ -48,6 +49,7 @@ function createProofStatusReport(projectRoot, taskId, mode = 'status') {
48
49
  schemaVersion: mode === 'explain' ? 'hadara.proof.explain.v1' : 'hadara.proof.status.v1',
49
50
  command: mode === 'explain' ? 'proof.explain' : 'proof.status',
50
51
  ok: blockers.length === 0,
52
+ taskId,
51
53
  target: { kind: 'task', taskId },
52
54
  claim: 'task-readiness',
53
55
  verdict,
@@ -32,7 +32,7 @@ function createTaskProtocolConsistencyReport(projectRoot, taskId, now = new Date
32
32
  taskId,
33
33
  message: `Task Capsule not found: ${taskId}`
34
34
  });
35
- return buildReport(projectRoot, now, issues, checkedDocs, undefined, null);
35
+ return buildReport(projectRoot, now, issues, checkedDocs, undefined, null, undefined, { taskId });
36
36
  }
37
37
  const capsulePath = toPortablePath(node_path_1.default.relative(projectRoot, task.dir));
38
38
  const taskStatus = readTaskStatus(task);
@@ -155,6 +155,7 @@ function buildReport(projectRoot, now, issues, checkedDocs, task, taskBoardStatu
155
155
  schemaVersion: 'hadara.protocol.consistency.v1',
156
156
  command: 'protocol.doctor',
157
157
  ok: counts.error === 0,
158
+ ...(options?.taskId ? { taskId: options.taskId } : task ? { taskId: task.id } : {}),
158
159
  scope: options?.scope ?? 'tasks',
159
160
  projectRoot,
160
161
  generatedAt: now.toISOString(),
@@ -33,6 +33,7 @@ function createProtocolMigrationReport(input) {
33
33
  schemaVersion: 'hadara.protocol.migration.v1',
34
34
  command: 'protocol.migrate',
35
35
  ok: issues.every((issue) => issue.severity !== 'error'),
36
+ ...(input.taskId ? { taskId: input.taskId } : {}),
36
37
  mode: input.mode,
37
38
  target: { protocolVersion: input.target },
38
39
  scope: { kind: input.taskId ? 'task' : 'project', taskId: input.taskId ?? null },
@@ -11,6 +11,7 @@ const node_path_1 = __importDefault(require("node:path"));
11
11
  const node_child_process_1 = require("node:child_process");
12
12
  const fs_1 = require("../core/fs");
13
13
  const schema_1 = require("../core/schema");
14
+ const timing_1 = require("../core/timing");
14
15
  const RELEASE_PACKAGE_DESCRIPTION = 'Portable AI-assisted development workbench for evidence-backed task capsules, handoffs, and release gates.';
15
16
  const RELEASE_PACKAGE_KEYWORDS = [
16
17
  'ai',
@@ -434,7 +435,7 @@ function cleanupDirectory(directory, cleanup) {
434
435
  node_fs_1.default.rmSync(directory, { recursive: true, force: true });
435
436
  }
436
437
  function runCommand(command, args, options) {
437
- const started = Date.now();
438
+ const timer = (0, timing_1.startMonotonicTimer)();
438
439
  const result = (0, node_child_process_1.spawnSync)(command, args, {
439
440
  cwd: options.cwd,
440
441
  env: options.env,
@@ -447,7 +448,7 @@ function runCommand(command, args, options) {
447
448
  signal: result.signal,
448
449
  stdout: result.stdout ?? '',
449
450
  stderr: result.stderr ?? '',
450
- elapsedMs: Date.now() - started,
451
+ elapsedMs: timer.elapsedMs(),
451
452
  timedOut: result.error?.name === 'TimeoutError' || result.signal === 'SIGTERM'
452
453
  };
453
454
  }
@@ -40,6 +40,7 @@ function createReleaseCloseoutReport(projectRoot, input) {
40
40
  schemaVersion: 'hadara.releaseCloseout.v1',
41
41
  command: 'release.closeout',
42
42
  ok: !issues.some((issue) => issue.severity === 'error'),
43
+ taskId,
43
44
  readOnly: true,
44
45
  generatedAt: new Date().toISOString(),
45
46
  input: { version, taskId },
@@ -3,15 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.timeStage = timeStage;
4
4
  exports.applyStageStatuses = applyStageStatuses;
5
5
  exports.createDiagnostics = createDiagnostics;
6
+ const timing_1 = require("../core/timing");
6
7
  function timeStage(timings, stage, fn) {
7
- const startedAt = Date.now();
8
+ const timer = (0, timing_1.startMonotonicTimer)();
8
9
  try {
9
10
  return fn();
10
11
  }
11
12
  finally {
12
13
  timings.push({
13
14
  stage,
14
- durationMs: Date.now() - startedAt,
15
+ durationMs: timer.elapsedMs(),
15
16
  status: 'passed',
16
17
  summary: `${stage} completed.`
17
18
  });
@@ -30,11 +31,11 @@ function applyStageStatuses(timings, releaseGateOk, checks) {
30
31
  }
31
32
  }
32
33
  }
33
- function createDiagnostics(generatedAt, startedAt, stageTimings, releaseTargetConfiguration) {
34
+ function createDiagnostics(generatedAt, timer, stageTimings, releaseTargetConfiguration) {
34
35
  const thresholdMs = 5000;
35
36
  return {
36
37
  generatedAt,
37
- durationMs: Date.now() - startedAt,
38
+ durationMs: timer.elapsedMs(),
38
39
  advisories: releaseTargetConfiguration.issues.map((issue) => ({
39
40
  area: 'release-target-configuration',
40
41
  severity: 'warning',