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.
- package/README.md +19 -19
- package/dist/cli/doctor.js +74 -3
- package/dist/cli/evidence-json.js +3 -0
- package/dist/cli/evidence.js +1 -0
- package/dist/cli/release-artifact.js +1 -1
- package/dist/cli/task.js +65 -11
- package/dist/cli/validation.js +14 -4
- package/dist/core/timing.js +12 -0
- package/dist/dev/docker-check.js +3 -2
- package/dist/evidence/evidence.js +3 -2
- package/dist/handoff/handoff-suggestion.js +1 -0
- package/dist/handoff/handoff.js +1 -0
- package/dist/harness/validate.js +140 -36
- package/dist/services/capability-registry.js +6 -5
- package/dist/services/ci-gate.js +1 -0
- package/dist/services/clean-checkout-smoke.js +4 -2
- package/dist/services/dashboard-bootstrap.js +16 -1
- package/dist/services/dashboard-task-detail.js +1 -0
- package/dist/services/package-recycle.js +5 -2
- package/dist/services/package-smoke.js +7 -2
- package/dist/services/proof-status.js +2 -0
- package/dist/services/protocol-consistency.js +2 -1
- package/dist/services/protocol-migration.js +1 -0
- package/dist/services/release-artifact.js +3 -2
- package/dist/services/release-closeout.js +1 -0
- package/dist/services/release-diagnostics.js +5 -4
- package/dist/services/release-dry-run.js +3 -2
- package/dist/services/task-read-model.js +4 -0
- package/dist/services/task-workbench.js +27 -11
- package/dist/services/validation-run.js +11 -3
- package/dist/services/write-preflight.js +2 -13
- package/dist/task/acceptance.js +27 -5
- package/dist/task/authoring-guidance.js +6 -4
- package/dist/task/task-capsule.js +20 -9
- package/dist/task/task-create.js +1 -0
- package/dist/task/task-templates.js +1 -1
- package/dist/task/task-upgrade-scaffold.js +8 -6
- package/dist/tui/read-model.js +2 -0
- package/package.json +1 -1
package/dist/harness/validate.js
CHANGED
|
@@ -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
|
|
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', '##
|
|
116
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
254
|
-
checkToken(
|
|
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 (
|
|
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: | ${
|
|
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 (!
|
|
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(
|
|
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.
|
|
1872
|
-
example('Execute installed-package recycle', 'hadara package recycle --execute --package hadara@latest --expected-version 0.
|
|
1873
|
-
example('Execute recycle with graph diagnostics', 'hadara package recycle --execute --package hadara@latest --expected-version 0.
|
|
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: [],
|
package/dist/services/ci-gate.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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,
|
|
34
|
+
function createDiagnostics(generatedAt, timer, stageTimings, releaseTargetConfiguration) {
|
|
34
35
|
const thresholdMs = 5000;
|
|
35
36
|
return {
|
|
36
37
|
generatedAt,
|
|
37
|
-
durationMs:
|
|
38
|
+
durationMs: timer.elapsedMs(),
|
|
38
39
|
advisories: releaseTargetConfiguration.issues.map((issue) => ({
|
|
39
40
|
area: 'release-target-configuration',
|
|
40
41
|
severity: 'warning',
|