hadara 0.3.2 → 0.3.3

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 (52) hide show
  1. package/README.md +49 -34
  2. package/dist/cli/context.js +110 -0
  3. package/dist/cli/help.js +6 -7
  4. package/dist/cli/init.js +56 -58
  5. package/dist/cli/main.js +12 -0
  6. package/dist/cli/session.js +37 -0
  7. package/dist/cli/task.js +52 -0
  8. package/dist/context/code-graph-extractor.js +123 -0
  9. package/dist/context/code-index.js +1154 -0
  10. package/dist/context/context-cache-store.js +925 -0
  11. package/dist/context/context-graph-builder.js +341 -0
  12. package/dist/context/context-graph.js +42 -0
  13. package/dist/context/context-pack.js +457 -0
  14. package/dist/context/context-slice-boundary.js +37 -0
  15. package/dist/context/context-slice.js +487 -0
  16. package/dist/context/document-extractors.js +343 -0
  17. package/dist/context/evidence-extractors.js +179 -0
  18. package/dist/context/extractor-contract.js +166 -0
  19. package/dist/context/registry-extractors.js +177 -0
  20. package/dist/context/release-extractors.js +175 -0
  21. package/dist/context/session-start.js +297 -0
  22. package/dist/context/source-manifest.js +566 -0
  23. package/dist/context/state-projection.js +209 -0
  24. package/dist/context/task-extractors.js +168 -0
  25. package/dist/core/schema.js +26 -0
  26. package/dist/harness/validate.js +7 -8
  27. package/dist/schemas/code-index.schema.json +173 -0
  28. package/dist/schemas/context-cache-record.schema.json +56 -0
  29. package/dist/schemas/context-cache-status.schema.json +167 -0
  30. package/dist/schemas/context-cache-warm.schema.json +222 -0
  31. package/dist/schemas/context-graph.schema.json +286 -0
  32. package/dist/schemas/context-pack.schema.json +246 -0
  33. package/dist/schemas/context-slice.schema.json +94 -0
  34. package/dist/schemas/context-source-manifest.schema.json +147 -0
  35. package/dist/schemas/schema-index.json +91 -0
  36. package/dist/schemas/session-start.schema.json +158 -0
  37. package/dist/schemas/task-close-repair-plan.schema.json +67 -0
  38. package/dist/schemas/task-context.schema.json +125 -0
  39. package/dist/schemas/task-finalize.schema.json +98 -0
  40. package/dist/schemas/task-lifecycle.schema.json +84 -0
  41. package/dist/services/capability-registry.js +266 -9
  42. package/dist/services/lifecycle-guide.js +23 -29
  43. package/dist/services/protocol-consistency.js +6 -8
  44. package/dist/services/workbench-next-actions.js +2 -0
  45. package/dist/task/acceptance.js +171 -0
  46. package/dist/task/task-close-repair-plan.js +190 -0
  47. package/dist/task/task-close.js +34 -35
  48. package/dist/task/task-finalize.js +377 -0
  49. package/dist/task/task-lifecycle.js +210 -0
  50. package/dist/task/task-next.js +10 -1
  51. package/dist/task/task-ready.js +4 -0
  52. package/package.json +1 -1
@@ -13,6 +13,7 @@ const protocol_profile_1 = require("./protocol-profile");
13
13
  const evidence_lint_1 = require("./evidence-lint");
14
14
  const markdown_table_1 = require("./markdown-table");
15
15
  const state_projection_1 = require("./state-projection");
16
+ const acceptance_1 = require("../task/acceptance");
16
17
  const task_capsule_1 = require("../task/task-capsule");
17
18
  const REQUIRED_TASK_FILES = Object.keys(task_capsule_1.TASK_FILES);
18
19
  const DONE_STATUSES = new Set(['done']);
@@ -656,25 +657,22 @@ function checkDoneAcceptance(projectRoot, task, taskLooksDone, issues) {
656
657
  if (!node_fs_1.default.existsSync(acceptancePath))
657
658
  return;
658
659
  const content = node_fs_1.default.readFileSync(acceptancePath, 'utf8');
659
- const rows = (0, markdown_table_1.parseMarkdownRows)(content).filter((cells) => /^AC-\d+$/i.test(cells[0] ?? ''));
660
- const pendingRows = rows.filter((cells) => {
661
- const status = cells[2]?.trim().toLowerCase();
662
- return !status || status === 'pending' || status === 'blocked';
663
- });
660
+ const acceptance = (0, acceptance_1.analyzeAcceptanceReadiness)(content);
664
661
  const checklistPending = content
665
662
  .split(/\r?\n/)
666
663
  .map((line) => line.trim())
667
664
  .some((line) => /^-\s+\[\s\]/.test(line));
668
- if (pendingRows.length > 0 || (rows.length === 0 && checklistPending)) {
665
+ if (acceptance.blockers.length > 0 || (acceptance.rows.length === 0 && checklistPending)) {
666
+ const blockerSummary = acceptance.blockers.length > 0 ? ` Blockers: ${acceptance.blockers.map((blocker) => `${blocker.code}(${blocker.row.id})`).join(', ')}.` : '';
669
667
  pushIssue(issues, {
670
668
  code: 'TASK_DONE_ACCEPTANCE_PENDING',
671
669
  severity: 'error',
672
670
  area: 'validation',
673
671
  taskId: task.id,
674
672
  path: toPortablePath(node_path_1.default.relative(projectRoot, acceptancePath)),
675
- message: 'Task is marked Done but ACCEPTANCE.md still has pending or blocked criteria.',
673
+ message: `Task is marked Done but ACCEPTANCE.md still has pending, blocked, or in-progress criteria.${blockerSummary}`,
676
674
  expected: 'all acceptance criteria complete',
677
- actual: 'pending criteria found'
675
+ actual: 'incomplete criteria found'
678
676
  });
679
677
  }
680
678
  }
@@ -5,6 +5,8 @@ exports.createWorkbenchNextAction = createWorkbenchNextAction;
5
5
  function buildWorkbenchNextActions(input) {
6
6
  const actions = new Map();
7
7
  for (const issue of input.issues) {
8
+ if (input.closed && issue.severity !== 'error' && issue.code.includes('HANDOFF'))
9
+ continue;
8
10
  addIssueAction(actions, input.taskId, issue);
9
11
  }
10
12
  if (input.evidenceRecords === 0) {
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeAcceptanceReadiness = analyzeAcceptanceReadiness;
4
+ exports.parseAcceptanceRows = parseAcceptanceRows;
5
+ const markdown_table_1 = require("../services/markdown-table");
6
+ function analyzeAcceptanceReadiness(content) {
7
+ const rows = parseAcceptanceRows(content);
8
+ const blockers = rows.flatMap(blockersForAcceptanceRow);
9
+ return {
10
+ rows,
11
+ blockers,
12
+ summary: {
13
+ total: rows.length,
14
+ required: rows.filter((row) => row.required).length,
15
+ met: rows.filter((row) => row.status === 'Met').length,
16
+ unresolvedRequired: blockers.filter((blocker) => blocker.code === 'ACCEPTANCE_REQUIRED_UNRESOLVED').length,
17
+ deferred: rows.filter((row) => row.status === 'Deferred').length,
18
+ followUps: rows.filter((row) => row.status === 'Follow-up Created').length,
19
+ acceptedRisks: rows.filter((row) => row.status === 'Accepted Risk').length
20
+ }
21
+ };
22
+ }
23
+ function parseAcceptanceRows(content) {
24
+ const rows = (0, markdown_table_1.parseMarkdownRows)(content);
25
+ const headerIndex = rows.findIndex((row) => headerIndexFor(row, 'id') >= 0 && headerIndexFor(row, 'status') >= 0);
26
+ const header = headerIndex >= 0 ? rows[headerIndex] : [];
27
+ const dataRows = (headerIndex >= 0 ? rows.slice(headerIndex + 1) : rows).filter((cells) => /^AC-\d+$/i.test(cells[0] ?? ''));
28
+ return dataRows.map((cells) => {
29
+ const allText = cells.join(' ');
30
+ const status = normalizeAcceptanceStatus(cellByHeader(cells, header, 'status') ?? cells[2] ?? '');
31
+ return {
32
+ id: cells[0] ?? '',
33
+ criterion: cellByHeader(cells, header, 'criterion') ?? cells[1] ?? '',
34
+ origin: normalizeAcceptanceOrigin(cellByHeader(cells, header, 'origin')),
35
+ required: normalizeBooleanCell(cellByHeader(cells, header, 'required'), true),
36
+ deferrable: normalizeBooleanCell(cellByHeader(cells, header, 'deferrable'), false),
37
+ status,
38
+ rawStatus: cellByHeader(cells, header, 'status') ?? cells[2] ?? '',
39
+ evidenceRefs: extractRefs(allText, /\bev:T-\d{4}:[A-Za-z0-9]+\b/g),
40
+ decisionRefs: extractRefs(allText, /\bD-[A-Za-z0-9._-]+\b/g),
41
+ riskRefs: extractRefs(allText, /\bR-[A-Za-z0-9._-]+\b/g),
42
+ followUpRefs: extractRefs(allText, /\b(?:T-\d{4}|DEBT-[A-Za-z0-9._-]+|GH-\d+)\b|hadara\s+task\s+create\s+["'`][^"'`]+["'`]/gi)
43
+ };
44
+ });
45
+ }
46
+ function blockersForAcceptanceRow(row) {
47
+ const blockers = [];
48
+ if (row.required && ['Not Met', 'In Progress', 'Pending', 'TBD', 'Blocked', 'Partial'].includes(row.status)) {
49
+ blockers.push({
50
+ row,
51
+ code: 'ACCEPTANCE_REQUIRED_UNRESOLVED',
52
+ message: `${row.id} is required but remains ${row.status}.`
53
+ });
54
+ }
55
+ if (row.status === 'Unknown') {
56
+ blockers.push({
57
+ row,
58
+ code: 'ACCEPTANCE_STATUS_UNKNOWN',
59
+ message: `${row.id} has unknown acceptance status "${row.rawStatus}".`
60
+ });
61
+ }
62
+ if (['Deferred', 'Follow-up Created', 'Accepted Risk'].includes(row.status)) {
63
+ if (row.required && !row.deferrable) {
64
+ blockers.push({
65
+ row,
66
+ code: 'ACCEPTANCE_NONDEFERRABLE_DEFERRED',
67
+ message: `${row.id} is required and non-deferrable, so ${row.status} cannot close as Done.`
68
+ });
69
+ }
70
+ if (row.decisionRefs.length === 0) {
71
+ blockers.push({
72
+ row,
73
+ code: 'ACCEPTANCE_DEFERRED_WITHOUT_DECISION',
74
+ message: `${row.id} is ${row.status} without a decision reference.`
75
+ });
76
+ }
77
+ }
78
+ if (row.status === 'Follow-up Created' && row.followUpRefs.length === 0) {
79
+ blockers.push({
80
+ row,
81
+ code: 'ACCEPTANCE_FOLLOWUP_WITHOUT_FOLLOWUP',
82
+ message: `${row.id} is Follow-up Created without a concrete follow-up reference.`
83
+ });
84
+ }
85
+ if (row.status === 'Accepted Risk' && row.riskRefs.length === 0) {
86
+ blockers.push({
87
+ row,
88
+ code: 'ACCEPTANCE_ACCEPTED_RISK_WITHOUT_RISK_RECORD',
89
+ message: `${row.id} is Accepted Risk without a risk reference.`
90
+ });
91
+ }
92
+ return blockers;
93
+ }
94
+ function cellByHeader(cells, header, name) {
95
+ const index = headerIndexFor(header, name);
96
+ return index >= 0 ? cells[index] : undefined;
97
+ }
98
+ function headerIndexFor(header, name) {
99
+ const expected = normalizeHeader(name);
100
+ return header.findIndex((cell) => normalizeHeader(cell) === expected);
101
+ }
102
+ function normalizeHeader(value) {
103
+ return value.toLowerCase().replace(/[^a-z0-9]/g, '');
104
+ }
105
+ function normalizeAcceptanceStatus(value) {
106
+ const normalized = value.trim().toLowerCase().replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
107
+ switch (normalized) {
108
+ case 'met':
109
+ case 'done':
110
+ case 'complete':
111
+ case 'completed':
112
+ return 'Met';
113
+ case 'not met':
114
+ return 'Not Met';
115
+ case 'in progress':
116
+ return 'In Progress';
117
+ case 'pending':
118
+ return 'Pending';
119
+ case 'tbd':
120
+ return 'TBD';
121
+ case 'deferred':
122
+ return 'Deferred';
123
+ case 'blocked':
124
+ return 'Blocked';
125
+ case 'partial':
126
+ return 'Partial';
127
+ case 'not applicable':
128
+ case 'n/a':
129
+ case 'na':
130
+ return 'Not Applicable';
131
+ case 'superseded':
132
+ return 'Superseded';
133
+ case 'follow up created':
134
+ case 'followup created':
135
+ return 'Follow-up Created';
136
+ case 'accepted risk':
137
+ return 'Accepted Risk';
138
+ default:
139
+ return 'Unknown';
140
+ }
141
+ }
142
+ function normalizeAcceptanceOrigin(value) {
143
+ const normalized = (value ?? '').trim().toLowerCase().replace(/[_\s]+/g, '-');
144
+ switch (normalized) {
145
+ case 'planned':
146
+ return 'planned';
147
+ case 'discovered':
148
+ return 'discovered';
149
+ case 'reviewer-feedback':
150
+ return 'reviewer-feedback';
151
+ case 'generated':
152
+ return 'generated';
153
+ case 'migration':
154
+ return 'migration';
155
+ default:
156
+ return 'original';
157
+ }
158
+ }
159
+ function normalizeBooleanCell(value, defaultValue) {
160
+ const normalized = (value ?? '').trim().toLowerCase();
161
+ if (!normalized)
162
+ return defaultValue;
163
+ if (['yes', 'y', 'true', '1', 'required'].includes(normalized))
164
+ return true;
165
+ if (['no', 'n', 'false', '0', 'optional'].includes(normalized))
166
+ return false;
167
+ return defaultValue;
168
+ }
169
+ function extractRefs(value, pattern) {
170
+ return [...new Set([...value.matchAll(pattern)].map((match) => match[0]))];
171
+ }
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTaskCloseRepairPlanReport = createTaskCloseRepairPlanReport;
4
+ exports.formatTaskCloseRepairPlanReport = formatTaskCloseRepairPlanReport;
5
+ const lifecycle_next_actions_1 = require("./lifecycle-next-actions");
6
+ const task_close_1 = require("./task-close");
7
+ function createTaskCloseRepairPlanReport(projectRoot, taskId, options = {}) {
8
+ const actor = options.actor ?? (0, lifecycle_next_actions_1.defaultTaskLifecycleActor)();
9
+ const reports = {
10
+ close: (0, task_close_1.createTaskCloseReport)(projectRoot, taskId, 'dry-run', { actor }),
11
+ audit: (0, task_close_1.createTaskAuditCloseReport)(projectRoot, taskId, { actor })
12
+ };
13
+ const classification = classifyRepair(reports);
14
+ const causes = collectCauses(reports, classification);
15
+ const nextActions = createRepairNextActions(taskId, classification, reports);
16
+ const issues = collectIssues(reports);
17
+ return {
18
+ schemaVersion: 'hadara.task.closeRepairPlan.v1',
19
+ command: 'task.close-repair-plan',
20
+ ok: !reports.close.issues.some((issue) => issue.code === 'TASK_NOT_FOUND') && !reports.audit.issues.some((issue) => issue.code === 'TASK_NOT_FOUND'),
21
+ readOnly: true,
22
+ taskId,
23
+ generatedAt: new Date().toISOString(),
24
+ actor,
25
+ classification,
26
+ repairNeeded: classification !== 'closed-valid',
27
+ summary: summarizeClassification(classification, reports),
28
+ evidence: createEvidenceSummary(reports.audit),
29
+ causes,
30
+ ...(nextActions[0] ? { primaryNextAction: nextActions[0] } : {}),
31
+ nextActions,
32
+ issues
33
+ };
34
+ }
35
+ function formatTaskCloseRepairPlanReport(report) {
36
+ const lines = [`[HADARA] task close-repair-plan ${report.taskId}: ${report.classification}`];
37
+ lines.push(`readOnly=${report.readOnly} repairNeeded=${report.repairNeeded} ok=${report.ok}`);
38
+ lines.push(report.summary);
39
+ if (report.primaryNextAction)
40
+ lines.push(`next=${report.primaryNextAction.command ?? report.primaryNextAction.summary ?? report.primaryNextAction.id}`);
41
+ for (const cause of report.causes)
42
+ lines.push(`[${cause.severity}] ${cause.code}: ${cause.summary}`);
43
+ return lines.join('\n');
44
+ }
45
+ function classifyRepair(reports) {
46
+ const audit = reports.audit;
47
+ if (reports.close.issues.some((issue) => issue.code === 'TASK_NOT_FOUND') || audit.issues.some((issue) => issue.code === 'TASK_NOT_FOUND'))
48
+ return 'unknown';
49
+ if (!audit.auditVerdict.closeEvidenceFound)
50
+ return 'not-closed';
51
+ if ((audit.closeEvidenceAudit?.duplicateCloseEvidenceCount ?? 0) > 0)
52
+ return 'duplicate-close-proof';
53
+ if (!audit.auditVerdict.closeEvidenceValid || audit.auditVerdict.verdict === 'close-evidence-invalid')
54
+ return 'closed-invalid';
55
+ if (audit.auditVerdict.reportHashMatches === false || audit.auditVerdict.sourceHashMatches === false || audit.auditVerdict.verdict === 'closed-with-drift-warnings')
56
+ return 'closed-stale';
57
+ if (audit.auditVerdict.verdict === 'closed-valid')
58
+ return 'closed-valid';
59
+ return 'unknown';
60
+ }
61
+ function summarizeClassification(classification, reports) {
62
+ switch (classification) {
63
+ case 'not-closed':
64
+ return 'No valid close evidence exists. Run ready if needed, then review and execute close.';
65
+ case 'closed-stale':
66
+ return 'Close evidence exists but recorded source or validation hashes no longer match current state.';
67
+ case 'closed-invalid':
68
+ return 'Close-like evidence exists but the latest close proof is invalid or malformed.';
69
+ case 'duplicate-close-proof':
70
+ return 'Multiple close proofs share conflicting or duplicate identity; inspect latest proof and append a fresh proof if needed.';
71
+ case 'closed-valid':
72
+ return 'Close proof is current and valid. No repair is needed.';
73
+ case 'unknown':
74
+ return reports.close.issues.some((issue) => issue.code === 'TASK_NOT_FOUND') ? 'Task was not found.' : 'Close repair state could not be classified.';
75
+ }
76
+ }
77
+ function createEvidenceSummary(audit) {
78
+ return {
79
+ closeEvidenceFound: audit.auditVerdict.closeEvidenceFound,
80
+ closeEvidenceValid: audit.auditVerdict.closeEvidenceValid,
81
+ closeEvidenceRecords: audit.summary.closeEvidenceRecords,
82
+ ...(audit.closeEvidenceAudit?.latestCloseEvidenceId ? { latestCloseEvidenceId: audit.closeEvidenceAudit.latestCloseEvidenceId } : {}),
83
+ ...(audit.closeEvidenceAudit ? { duplicateCloseEvidenceCount: audit.closeEvidenceAudit.duplicateCloseEvidenceCount, supersededCloseEvidenceIds: audit.closeEvidenceAudit.supersededCloseEvidenceIds } : {}),
84
+ ...(audit.auditVerdict.recordedValidationReportHash ? { recordedValidationReportHash: audit.auditVerdict.recordedValidationReportHash } : {}),
85
+ currentValidationReportHash: audit.auditVerdict.currentValidationReportHash,
86
+ ...(typeof audit.auditVerdict.reportHashMatches === 'boolean' ? { reportHashMatches: audit.auditVerdict.reportHashMatches } : {}),
87
+ ...(audit.auditVerdict.recordedSourceHash ? { recordedSourceHash: audit.auditVerdict.recordedSourceHash } : {}),
88
+ currentSourceHash: audit.auditVerdict.currentSourceHash,
89
+ ...(typeof audit.auditVerdict.sourceHashMatches === 'boolean' ? { sourceHashMatches: audit.auditVerdict.sourceHashMatches } : {})
90
+ };
91
+ }
92
+ function collectCauses(reports, classification) {
93
+ const causes = [];
94
+ if (classification === 'closed-valid') {
95
+ causes.push({ code: 'CLOSE_REPAIR_NOT_NEEDED', severity: 'info', summary: 'Audit reports closed-valid.', sourceReport: 'hadara.task.audit_close.v1' });
96
+ return causes;
97
+ }
98
+ if (classification === 'not-closed') {
99
+ causes.push({ code: 'CLOSE_REPAIR_NOT_CLOSED', severity: 'error', summary: 'No close evidence was found.', sourceReport: 'hadara.task.audit_close.v1' });
100
+ }
101
+ if (classification === 'duplicate-close-proof') {
102
+ causes.push({
103
+ code: 'CLOSE_REPAIR_DUPLICATE_PROOF',
104
+ severity: 'warning',
105
+ summary: `${reports.audit.closeEvidenceAudit?.duplicateCloseEvidenceCount ?? 0} duplicate close proof record(s) were found.`,
106
+ sourceReport: 'hadara.task.audit_close.v1'
107
+ });
108
+ }
109
+ if (reports.audit.auditVerdict.reportHashMatches === false) {
110
+ causes.push({ code: 'CLOSE_REPAIR_REPORT_HASH_DRIFT', severity: 'warning', summary: 'Recorded validation report hash differs from the current validation hash.', sourceReport: 'hadara.task.audit_close.v1' });
111
+ }
112
+ if (reports.audit.auditVerdict.sourceHashMatches === false) {
113
+ causes.push({ code: 'CLOSE_REPAIR_SOURCE_HASH_DRIFT', severity: 'warning', summary: 'Recorded close-source hash differs from the current close-source hash.', sourceReport: 'hadara.task.audit_close.v1' });
114
+ }
115
+ if (!reports.audit.auditVerdict.closeEvidenceValid && reports.audit.auditVerdict.closeEvidenceFound) {
116
+ causes.push({ code: 'CLOSE_REPAIR_INVALID_PROOF', severity: 'error', summary: 'Latest close evidence is not a passed command-log close proof.', sourceReport: 'hadara.task.audit_close.v1' });
117
+ }
118
+ for (const issue of reports.audit.issues) {
119
+ if (causes.some((cause) => cause.code === issue.code))
120
+ continue;
121
+ causes.push({ code: issue.code, severity: issue.severity, summary: issue.message, sourceReport: 'hadara.task.audit_close.v1', ...(issue.path ? { path: issue.path } : {}) });
122
+ }
123
+ if (causes.length === 0)
124
+ causes.push({ code: 'CLOSE_REPAIR_UNKNOWN', severity: 'warning', summary: 'Repair state is not closed-valid but no specific cause was available.', sourceReport: 'hadara.task.audit_close.v1' });
125
+ return causes;
126
+ }
127
+ function createRepairNextActions(taskId, classification, reports) {
128
+ if (classification === 'closed-valid')
129
+ return [];
130
+ if (classification === 'unknown') {
131
+ return [
132
+ (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
133
+ id: 'inspect-task',
134
+ kind: 'review',
135
+ required: true,
136
+ message: `Inspect task ${taskId} before repairing close state.`,
137
+ writeBoundary: 'read-only',
138
+ recommendedActorRole: 'reviewer',
139
+ requiresBeforeHash: false,
140
+ stalePlanRisk: 'none'
141
+ })
142
+ ];
143
+ }
144
+ const actions = [];
145
+ if (!reports.close.ok) {
146
+ actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
147
+ id: 'run-ready',
148
+ required: true,
149
+ command: `hadara task ready --task ${taskId} --level done --json`,
150
+ message: 'Resolve readiness blockers before appending a fresh close proof.',
151
+ writeBoundary: 'read-only',
152
+ recommendedActorRole: 'worker',
153
+ requiresBeforeHash: false,
154
+ stalePlanRisk: 'none'
155
+ }));
156
+ }
157
+ actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
158
+ id: 'review-close-plan',
159
+ required: true,
160
+ command: `hadara task close --task ${taskId} --json`,
161
+ message: 'Review the current close plan before appending or replacing close evidence.',
162
+ writeBoundary: 'read-only',
163
+ recommendedActorRole: 'worker',
164
+ requiresBeforeHash: false,
165
+ stalePlanRisk: classification === 'closed-stale' ? 'medium' : 'low'
166
+ }));
167
+ actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
168
+ id: 'audit-after-repair',
169
+ required: false,
170
+ command: `hadara task audit-close --task ${taskId} --json`,
171
+ message: 'After close execute, audit the repaired close proof.',
172
+ writeBoundary: 'read-only',
173
+ recommendedActorRole: 'reviewer',
174
+ requiresBeforeHash: false,
175
+ stalePlanRisk: 'none'
176
+ }));
177
+ return actions;
178
+ }
179
+ function collectIssues(reports) {
180
+ const seen = new Set();
181
+ const issues = [];
182
+ for (const issue of [...reports.close.issues, ...reports.audit.issues]) {
183
+ const key = `${issue.severity}:${issue.code}:${issue.path ?? ''}:${issue.message}`;
184
+ if (seen.has(key))
185
+ continue;
186
+ seen.add(key);
187
+ issues.push({ severity: issue.severity, code: issue.code, message: issue.message, ...(issue.path ? { path: issue.path } : {}) });
188
+ }
189
+ return issues;
190
+ }
@@ -175,7 +175,36 @@ function createNextActions(taskId, ok, mode, closeEvidenceWrite) {
175
175
  }
176
176
  ];
177
177
  }
178
- const actions = [
178
+ if (ok) {
179
+ if (closeEvidenceWrite?.duplicateAction === 'no-op') {
180
+ return [
181
+ (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
182
+ id: 'audit-close',
183
+ required: false,
184
+ command: `hadara task audit-close --task ${taskId} --json`,
185
+ message: 'Matching close evidence already exists; audit the existing close record.',
186
+ writeBoundary: 'read-only',
187
+ recommendedActorRole: 'reviewer',
188
+ requiresBeforeHash: false,
189
+ stalePlanRisk: 'none'
190
+ })
191
+ ];
192
+ }
193
+ return [
194
+ (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
195
+ id: 'append-close-evidence',
196
+ required: true,
197
+ command: `hadara task close --task ${taskId} --execute --json`,
198
+ message: 'Append close audit evidence after reviewing this dry-run plan.',
199
+ writeBoundary: 'evidence-append',
200
+ recommendedActorRole: 'worker',
201
+ requiresBeforeHash: false,
202
+ stalePlanRisk: 'low',
203
+ loopBoundary: true
204
+ })
205
+ ];
206
+ }
207
+ return [
179
208
  (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
180
209
  id: 'run-done-validation',
181
210
  required: true,
@@ -195,37 +224,8 @@ function createNextActions(taskId, ok, mode, closeEvidenceWrite) {
195
224
  recommendedActorRole: 'worker',
196
225
  requiresBeforeHash: false,
197
226
  stalePlanRisk: 'none'
198
- })
199
- ];
200
- if (ok) {
201
- if (closeEvidenceWrite?.duplicateAction === 'no-op') {
202
- actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
203
- id: 'audit-close',
204
- required: false,
205
- command: `hadara task audit-close --task ${taskId} --json`,
206
- message: 'Matching close evidence already exists; audit the existing close record.',
207
- writeBoundary: 'read-only',
208
- recommendedActorRole: 'reviewer',
209
- requiresBeforeHash: false,
210
- stalePlanRisk: 'none'
211
- }));
212
- }
213
- else {
214
- actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
215
- id: 'append-close-evidence',
216
- required: true,
217
- command: `hadara task close --task ${taskId} --execute --json`,
218
- message: 'Append close audit evidence after reviewing this dry-run plan.',
219
- writeBoundary: 'evidence-append',
220
- recommendedActorRole: 'worker',
221
- requiresBeforeHash: false,
222
- stalePlanRisk: 'low',
223
- loopBoundary: true
224
- }));
225
- }
226
- }
227
- else {
228
- actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
227
+ }),
228
+ (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
229
229
  id: 'resolve-close-blockers',
230
230
  required: true,
231
231
  message: 'Resolve blocking issues before appending close evidence.',
@@ -234,9 +234,8 @@ function createNextActions(taskId, ok, mode, closeEvidenceWrite) {
234
234
  requiresBeforeHash: false,
235
235
  stalePlanRisk: 'none',
236
236
  kind: 'review'
237
- }));
238
- }
239
- return actions;
237
+ })
238
+ ];
240
239
  }
241
240
  function buildMissingTaskReport(projectRoot, taskId, mode, issues, actor = (0, lifecycle_next_actions_1.defaultTaskLifecycleActor)()) {
242
241
  return {