hadara 0.3.3 → 0.3.4-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createReleaseCloseoutReport = createReleaseCloseoutReport;
7
+ exports.formatReleaseCloseoutReport = formatReleaseCloseoutReport;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const task_capsule_1 = require("../task/task-capsule");
11
+ const SHARED_FILES = [
12
+ { path: 'docs/RELEASE_READINESS.md', kind: 'release-readiness', role: 'source-readiness' },
13
+ { path: 'docs/RELEASE_NOTES.md', kind: 'release-notes', role: 'publish' },
14
+ { path: 'docs/PROJECT_STATE.md', kind: 'project-state', role: 'shared-state' },
15
+ { path: 'docs/AGENT_HANDOFF.md', kind: 'agent-handoff', role: 'shared-state' },
16
+ { path: 'docs/TASK_BOARD.md', kind: 'task-board', role: 'shared-state' },
17
+ { path: 'docs/DEVELOPMENT_SLICES.md', kind: 'development-slices', role: 'shared-state' }
18
+ ];
19
+ const CAPSULE_FILES = ['TASK.md', 'ACCEPTANCE.md', 'TESTS.md', 'EVIDENCE.md', 'HANDOFF.md'];
20
+ function createReleaseCloseoutReport(projectRoot, input) {
21
+ const version = input.version ?? null;
22
+ const taskId = input.taskId ?? null;
23
+ const issues = [];
24
+ if (!version)
25
+ issues.push({ severity: 'error', code: 'RELEASE_CLOSEOUT_VERSION_REQUIRED', message: 'release closeout requires --version <version>.' });
26
+ if (!taskId)
27
+ issues.push({ severity: 'error', code: 'RELEASE_CLOSEOUT_TASK_REQUIRED', message: 'release closeout requires --task <task-id>.' });
28
+ const task = taskId ? (0, task_capsule_1.listTaskCapsules)(projectRoot).find((candidate) => candidate.id === taskId) : undefined;
29
+ if (taskId && !task)
30
+ issues.push({ severity: 'warning', code: 'RELEASE_CLOSEOUT_TASK_NOT_FOUND', message: `Task Capsule not found: ${taskId}.`, path: `tasks/${taskId}` });
31
+ const surfaces = [
32
+ ...SHARED_FILES.map((surface) => analyzeSharedSurface(projectRoot, surface, version, taskId)),
33
+ ...CAPSULE_FILES.map((fileName) => analyzeCapsuleSurface(projectRoot, task?.dir, fileName, version, taskId))
34
+ ];
35
+ const suggestedFragments = createSuggestedFragments(version, taskId);
36
+ const current = surfaces.filter((surface) => surface.status === 'current').length;
37
+ const stale = surfaces.filter((surface) => surface.status === 'stale').length;
38
+ const missing = surfaces.filter((surface) => surface.status === 'missing').length;
39
+ return {
40
+ schemaVersion: 'hadara.releaseCloseout.v1',
41
+ command: 'release.closeout',
42
+ ok: !issues.some((issue) => issue.severity === 'error'),
43
+ readOnly: true,
44
+ generatedAt: new Date().toISOString(),
45
+ input: { version, taskId },
46
+ summary: {
47
+ files: surfaces.length,
48
+ current,
49
+ stale,
50
+ missing,
51
+ suggestedFragments: suggestedFragments.length
52
+ },
53
+ surfaces,
54
+ suggestedFragments,
55
+ issues
56
+ };
57
+ }
58
+ function formatReleaseCloseoutReport(report) {
59
+ const lines = [`[HADARA] release closeout ${report.input.version ?? '(missing-version)'}: ${report.ok ? 'ok' : 'issues'}`];
60
+ lines.push(`files=${report.summary.files} current=${report.summary.current} stale=${report.summary.stale} missing=${report.summary.missing}`);
61
+ for (const surface of report.surfaces)
62
+ lines.push(`${surface.status}\t${surface.path}\t${surface.summary}`);
63
+ for (const issue of report.issues)
64
+ lines.push(`[${issue.severity}] ${issue.code}: ${issue.message}`);
65
+ return lines.join('\n');
66
+ }
67
+ function analyzeSharedSurface(projectRoot, surface, version, taskId) {
68
+ const absolutePath = node_path_1.default.join(projectRoot, surface.path);
69
+ const content = node_fs_1.default.existsSync(absolutePath) ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : '';
70
+ const expectedSignals = expectedSharedSignals(surface.kind, version, taskId);
71
+ return buildSurface(surface, content, expectedSignals);
72
+ }
73
+ function analyzeCapsuleSurface(projectRoot, taskDir, fileName, version, taskId) {
74
+ const relativePath = taskDir ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(taskDir, fileName))) : `tasks/${taskId ?? 'T-XXXX'}/${fileName}`;
75
+ const absolutePath = taskDir ? node_path_1.default.join(taskDir, fileName) : '';
76
+ const content = absolutePath && node_fs_1.default.existsSync(absolutePath) ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : '';
77
+ return buildSurface({
78
+ path: relativePath,
79
+ kind: 'task-capsule',
80
+ role: 'capsule'
81
+ }, content, [taskId, version].filter((value) => Boolean(value)));
82
+ }
83
+ function buildSurface(surface, content, expectedSignals) {
84
+ const matchedSignals = expectedSignals.filter((signal) => content.includes(signal));
85
+ const missingSignals = expectedSignals.filter((signal) => !content.includes(signal));
86
+ const status = !content ? 'missing' : missingSignals.length === 0 ? 'current' : 'stale';
87
+ return {
88
+ ...surface,
89
+ status,
90
+ expectedSignals,
91
+ matchedSignals,
92
+ missingSignals,
93
+ summary: status === 'current' ? 'Expected closeout signals are present.' : status === 'missing' ? 'File is missing or task capsule was not found.' : `Missing closeout signals: ${missingSignals.join(', ')}.`
94
+ };
95
+ }
96
+ function expectedSharedSignals(kind, version, taskId) {
97
+ const signals = [version, taskId].filter((value) => Boolean(value));
98
+ if (kind === 'release-readiness')
99
+ return signals;
100
+ if (kind === 'release-notes')
101
+ return [version].filter((value) => Boolean(value));
102
+ return signals;
103
+ }
104
+ function createSuggestedFragments(version, taskId) {
105
+ const displayVersion = version ?? '<version>';
106
+ const displayTask = taskId ?? '<task-id>';
107
+ return [
108
+ {
109
+ path: 'docs/RELEASE_READINESS.md',
110
+ sectionHint: 'Current release section',
111
+ purpose: 'Record source readiness, publish, GitHub Release, and installed-package recycle state.',
112
+ suggestedMarkdown: `| ${displayVersion} closeout | ${displayTask} | Record readiness, publish verification, GitHub Release decision, installed-package recycle, and residual risks. |`
113
+ },
114
+ {
115
+ path: 'docs/RELEASE_NOTES.md',
116
+ sectionHint: displayVersion,
117
+ purpose: 'Summarize the released version for package consumers.',
118
+ suggestedMarkdown: `## ${displayVersion}\n\n- Summarize user-facing changes.\n- Record npm publish/recycle status and any explicit deferrals.`
119
+ },
120
+ {
121
+ path: 'docs/PROJECT_STATE.md',
122
+ sectionHint: 'Follow-up note',
123
+ purpose: 'Carry forward release state into current project status.',
124
+ suggestedMarkdown: `${displayTask} follow-up note: ${displayVersion} release closeout state was reviewed. Record readiness, publish, GitHub Release, recycle, and next release-line decision.`
125
+ },
126
+ {
127
+ path: 'docs/AGENT_HANDOFF.md',
128
+ sectionHint: 'Current State / Last 3 Completed Tasks',
129
+ purpose: 'Route the next agent after release closeout.',
130
+ suggestedMarkdown: `| Latest Completed Task | ${displayTask} ${displayVersion} release closeout | Record closeout result and next release-line task. |`
131
+ }
132
+ ];
133
+ }
134
+ function toPortablePath(value) {
135
+ return value.split(node_path_1.default.sep).join('/');
136
+ }
@@ -14,7 +14,7 @@ function createTaskFinalizeReport(projectRoot, taskId, options = {}) {
14
14
  const actor = options.actor ?? (0, lifecycle_next_actions_1.defaultTaskLifecycleActor)();
15
15
  const reports = createFinalizeReports(projectRoot, taskId, actor);
16
16
  const steps = createSteps(taskId, reports);
17
- const issues = collectIssues(reports);
17
+ const issues = collectIssues(taskId, reports);
18
18
  const planHash = hashPlan(taskId, steps);
19
19
  if (options.executeRequested)
20
20
  return executeFinalizePlan(projectRoot, taskId, actor, reports, steps, issues, planHash, options.planHash);
@@ -100,7 +100,7 @@ function executeFinalizePlan(projectRoot, taskId, actor, initialReports, initial
100
100
  function createPostExecutionReport(projectRoot, taskId, actor, requestedPlanHash, reviewedPlanHash, executedSteps, stoppedAt) {
101
101
  const reports = createFinalizeReports(projectRoot, taskId, actor);
102
102
  const steps = createSteps(taskId, reports);
103
- const issues = collectIssues(reports);
103
+ const issues = collectIssues(taskId, reports);
104
104
  const nextAction = createPrimaryNextAction(taskId, steps, issues);
105
105
  const finalAudit = reports.audit?.auditVerdict.verdict === 'closed-valid';
106
106
  const execution = {
@@ -178,7 +178,7 @@ function createSteps(taskId, reports) {
178
178
  ? 'required'
179
179
  : 'blocked'
180
180
  : 'pending';
181
- const auditStatus = reports.audit ? (reports.audit.auditVerdict.closeEvidenceFound ? (reports.audit.ok ? 'satisfied' : 'required') : 'pending') : 'pending';
181
+ const auditStatus = reports.audit ? (reports.audit.auditVerdict.closeEvidenceFound ? (reports.audit.auditVerdict.verdict === 'closed-valid' ? 'satisfied' : 'required') : 'pending') : 'pending';
182
182
  return [
183
183
  {
184
184
  id: 'finish',
@@ -216,7 +216,13 @@ function createSteps(taskId, reports) {
216
216
  {
217
217
  id: 'audit-close',
218
218
  status: auditStatus,
219
- summary: auditStatus === 'satisfied' ? 'Close audit passed.' : auditStatus === 'pending' ? 'Audit waits for close evidence.' : 'Run close audit and repair any drift.',
219
+ summary: auditStatus === 'satisfied'
220
+ ? 'Close audit passed.'
221
+ : auditStatus === 'pending'
222
+ ? 'Audit waits for close evidence.'
223
+ : reports.audit?.auditVerdict.verdict === 'closed-with-drift-warnings'
224
+ ? 'Close audit found close-source drift; review repair plan, then append fresh close proof.'
225
+ : 'Run close audit and repair any drift.',
220
226
  command: `hadara task audit-close --task ${taskId} --json`,
221
227
  mode: 'read-only',
222
228
  writeBoundary: 'read-only',
@@ -226,7 +232,7 @@ function createSteps(taskId, reports) {
226
232
  }
227
233
  ];
228
234
  }
229
- function collectIssues(reports) {
235
+ function collectIssues(taskId, reports) {
230
236
  const seen = new Set();
231
237
  const issues = [];
232
238
  const reportIssues = [...reports.finish.issues, ...(reports.ready?.issues ?? []), ...(reports.close?.issues ?? []), ...(reports.audit?.issues ?? [])];
@@ -255,6 +261,15 @@ function collectIssues(reports) {
255
261
  example: 'hadara evidence add-command --task T-XXXX --summary "Focused validation passed." --result passed --category validation --json'
256
262
  });
257
263
  }
264
+ if (issues.some(isCloseDriftIssue)) {
265
+ issues.push({
266
+ severity: 'info',
267
+ code: 'TASK_FINALIZE_CLOSE_SOURCE_DRIFT_GUIDANCE',
268
+ message: 'Close-source files changed after the recorded close proof. Review the repair plan, finish intended doc edits, then append a fresh close proof and audit it.',
269
+ fixHint: 'Use close-repair-plan for the read-only diagnosis, then rerun the guarded close/finalize path after close-source edits are complete.',
270
+ example: `hadara task close-repair-plan --task ${taskId} --json`
271
+ });
272
+ }
258
273
  return issues;
259
274
  }
260
275
  function createPrimaryNextAction(taskId, steps, issues) {
@@ -274,6 +289,19 @@ function createPrimaryNextAction(taskId, steps, issues) {
274
289
  stalePlanRisk: 'low'
275
290
  });
276
291
  }
292
+ if (nextStep.id === 'audit-close' && issues.some(isCloseDriftIssue)) {
293
+ return (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
294
+ id: 'finalize-review-close-repair-plan',
295
+ kind: 'command',
296
+ required: true,
297
+ command: `hadara task close-repair-plan --task ${taskId} --json`,
298
+ message: 'Close-source drift was detected. Review the close repair plan before appending fresh close proof.',
299
+ writeBoundary: 'read-only',
300
+ recommendedActorRole: 'reviewer',
301
+ requiresBeforeHash: false,
302
+ stalePlanRisk: 'none'
303
+ });
304
+ }
277
305
  return (0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
278
306
  id: `finalize-${nextStep.id}`,
279
307
  kind: nextStep.status === 'blocked' ? 'review' : 'command',
@@ -286,6 +314,9 @@ function createPrimaryNextAction(taskId, steps, issues) {
286
314
  stalePlanRisk: nextStep.writeBoundary === 'read-only' ? 'none' : 'low'
287
315
  });
288
316
  }
317
+ function isCloseDriftIssue(issue) {
318
+ return issue.code === 'TASK_CLOSE_AUDIT_SOURCE_HASH_DRIFT' || issue.code === 'TASK_CLOSE_AUDIT_CURRENT_REPORT_HASH_DRIFT';
319
+ }
289
320
  function summarizeSteps(steps, reports) {
290
321
  const evaluatedReports = evaluatedReportNames(steps, reports);
291
322
  const skippedReports = ['finish', 'ready', 'close', 'audit-close'].filter((name) => !evaluatedReports.includes(name));
@@ -61,7 +61,7 @@ function createChecks(taskId, reports) {
61
61
  status: reports.finish.ok ? (reports.finish.summary.plannedWrites > 0 ? 'required' : 'satisfied') : 'blocked',
62
62
  summary: reports.finish.summary.plannedWrites > 0 ? 'Task status bookkeeping has planned writes.' : reports.finish.ok ? 'Task finish bookkeeping is current.' : 'Task finish blockers exist.',
63
63
  sourceReport: 'hadara.task.finish.v1',
64
- command: reports.finish.summary.plannedWrites > 0 ? `hadara task finish --task ${taskId} --json` : undefined
64
+ ...(reports.finish.summary.plannedWrites > 0 ? { command: `hadara task finish --task ${taskId} --json` } : {})
65
65
  },
66
66
  sharedDocs: {
67
67
  status: sharedDocsPending.length > 0 ? 'warning' : reports.finish.ok ? 'satisfied' : 'pending',
@@ -78,11 +78,11 @@ function createChecks(taskId, reports) {
78
78
  status: reports.audit.auditVerdict.closeEvidenceFound ? 'satisfied' : reports.close.ok ? 'required' : 'blocked',
79
79
  summary: reports.audit.auditVerdict.closeEvidenceFound ? 'Close evidence exists.' : reports.close.ok ? 'Close evidence has not been appended.' : 'Close preconditions have blockers.',
80
80
  sourceReport: 'hadara.task.close.v1',
81
- command: reports.audit.auditVerdict.closeEvidenceFound ? undefined : `hadara task close --task ${taskId} --json`
81
+ ...(reports.audit.auditVerdict.closeEvidenceFound ? {} : { command: `hadara task close --task ${taskId} --json` })
82
82
  },
83
83
  audit: {
84
- status: reports.audit.ok ? 'satisfied' : reports.audit.auditVerdict.closeEvidenceFound ? 'required' : 'pending',
85
- summary: reports.audit.ok ? 'Close audit passed.' : reports.audit.auditVerdict.closeEvidenceFound ? 'Close audit requires repair.' : 'Audit waits for close evidence.',
84
+ status: reports.audit.auditVerdict.verdict === 'closed-valid' ? 'satisfied' : reports.audit.auditVerdict.closeEvidenceFound ? 'required' : 'pending',
85
+ summary: reports.audit.auditVerdict.verdict === 'closed-valid' ? 'Close audit passed.' : reports.audit.auditVerdict.closeEvidenceFound ? 'Close audit requires repair.' : 'Audit waits for close evidence.',
86
86
  sourceReport: 'hadara.task.audit_close.v1',
87
87
  command: `hadara task audit-close --task ${taskId} --json`
88
88
  }
@@ -103,7 +103,7 @@ function choosePhase(reports, checks, repair) {
103
103
  return 'ready-required';
104
104
  if (!reports.audit.auditVerdict.closeEvidenceFound)
105
105
  return 'close-required';
106
- if (reports.audit.ok)
106
+ if (reports.audit.auditVerdict.verdict === 'closed-valid')
107
107
  return 'closed-valid';
108
108
  if (repair && repair.classification !== 'closed-valid')
109
109
  return 'repair-required';
@@ -133,7 +133,7 @@ function chooseNextAction(taskId, phase, reports, checks, repair) {
133
133
  return undefined;
134
134
  }
135
135
  function createRepair(taskId, audit) {
136
- if (audit.ok) {
136
+ if (audit.auditVerdict.verdict === 'closed-valid') {
137
137
  return {
138
138
  classification: 'closed-valid',
139
139
  summary: 'Close proof is current and valid.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadara",
3
- "version": "0.3.3",
3
+ "version": "0.3.4-rc.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Portable AI-assisted development workbench for evidence-backed task capsules, handoffs, and release gates.",