hadara 0.1.0-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.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/agent/evidence.js +50 -0
  4. package/dist/agent/loop.js +124 -0
  5. package/dist/cli/args.js +70 -0
  6. package/dist/cli/dashboard.js +185 -0
  7. package/dist/cli/debt.js +41 -0
  8. package/dist/cli/doctor.js +68 -0
  9. package/dist/cli/errors.js +58 -0
  10. package/dist/cli/evidence-json.js +75 -0
  11. package/dist/cli/evidence.js +80 -0
  12. package/dist/cli/handoff.js +16 -0
  13. package/dist/cli/harness.js +57 -0
  14. package/dist/cli/hermes-json.js +31 -0
  15. package/dist/cli/hermes.js +28 -0
  16. package/dist/cli/init.js +142 -0
  17. package/dist/cli/install.js +34 -0
  18. package/dist/cli/main.js +216 -0
  19. package/dist/cli/mcp.js +15 -0
  20. package/dist/cli/package-smoke.js +37 -0
  21. package/dist/cli/policy-json.js +22 -0
  22. package/dist/cli/policy.js +43 -0
  23. package/dist/cli/release-artifact.js +47 -0
  24. package/dist/cli/release-dry-run.js +24 -0
  25. package/dist/cli/release-gate.js +28 -0
  26. package/dist/cli/release-publish.js +41 -0
  27. package/dist/cli/run-scaffold.js +68 -0
  28. package/dist/cli/run-state.js +41 -0
  29. package/dist/cli/run.js +191 -0
  30. package/dist/cli/smoke.js +58 -0
  31. package/dist/cli/status-json.js +6 -0
  32. package/dist/cli/status.js +26 -0
  33. package/dist/cli/task-json.js +8 -0
  34. package/dist/cli/task.js +64 -0
  35. package/dist/cli/tools.js +25 -0
  36. package/dist/cli/tui.js +72 -0
  37. package/dist/cli/write-preflight.js +27 -0
  38. package/dist/core/audit.js +41 -0
  39. package/dist/core/events.js +63 -0
  40. package/dist/core/fs.js +44 -0
  41. package/dist/core/paths.js +59 -0
  42. package/dist/core/redaction.js +178 -0
  43. package/dist/core/schema.js +253 -0
  44. package/dist/core/workspace.js +47 -0
  45. package/dist/evidence/evidence.js +170 -0
  46. package/dist/evidence/private-manifest.js +101 -0
  47. package/dist/handoff/handoff.js +49 -0
  48. package/dist/harness/replay.js +200 -0
  49. package/dist/harness/validate.js +465 -0
  50. package/dist/hermes/context-export.js +104 -0
  51. package/dist/index.js +29 -0
  52. package/dist/mcp/server.js +104 -0
  53. package/dist/mcp/tool-dispatch.js +159 -0
  54. package/dist/mcp/tool-registry.js +150 -0
  55. package/dist/mcp/tool-schemas.js +18 -0
  56. package/dist/policy/command-risk.js +39 -0
  57. package/dist/policy/permission-matrix.js +42 -0
  58. package/dist/policy/policy.js +20 -0
  59. package/dist/policy/preflight.js +47 -0
  60. package/dist/policy/presets.js +24 -0
  61. package/dist/policy/tokenizer.js +53 -0
  62. package/dist/providers/fallback-executor.js +46 -0
  63. package/dist/providers/mock-provider.js +49 -0
  64. package/dist/providers/provider-contract.js +2 -0
  65. package/dist/providers/provider-preparation.js +220 -0
  66. package/dist/providers/scripted-provider.js +69 -0
  67. package/dist/schemas/active-run-projection.schema.json +73 -0
  68. package/dist/schemas/active-run-resume.schema.json +68 -0
  69. package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
  70. package/dist/schemas/context-export.schema.json +35 -0
  71. package/dist/schemas/event.schema.json +17 -0
  72. package/dist/schemas/evidence-list.schema.json +49 -0
  73. package/dist/schemas/feature-smoke.schema.json +67 -0
  74. package/dist/schemas/install-plan.schema.json +93 -0
  75. package/dist/schemas/package-smoke.schema.json +130 -0
  76. package/dist/schemas/private-evidence.schema.json +48 -0
  77. package/dist/schemas/provider-call.schema.json +42 -0
  78. package/dist/schemas/provider-config.schema.json +43 -0
  79. package/dist/schemas/release-artifact-manifest.schema.json +55 -0
  80. package/dist/schemas/release-artifact.schema.json +140 -0
  81. package/dist/schemas/release-dry-run.schema.json +141 -0
  82. package/dist/schemas/release-gate.schema.json +42 -0
  83. package/dist/schemas/release-publish.schema.json +114 -0
  84. package/dist/schemas/schema-index.json +145 -0
  85. package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
  86. package/dist/schemas/tools-list.schema.json +78 -0
  87. package/dist/schemas/write-preflight.schema.json +47 -0
  88. package/dist/services/active-run-state.js +215 -0
  89. package/dist/services/capability-registry.js +540 -0
  90. package/dist/services/clean-checkout-smoke.js +393 -0
  91. package/dist/services/evidence-list.js +136 -0
  92. package/dist/services/feature-smoke.js +155 -0
  93. package/dist/services/harness-service.js +7 -0
  94. package/dist/services/install-plan.js +233 -0
  95. package/dist/services/operational-debt.js +767 -0
  96. package/dist/services/operations-status-service.js +195 -0
  97. package/dist/services/package-smoke.js +676 -0
  98. package/dist/services/policy-service.js +25 -0
  99. package/dist/services/project-read-model.js +101 -0
  100. package/dist/services/release-artifact-evidence.js +77 -0
  101. package/dist/services/release-artifact.js +351 -0
  102. package/dist/services/release-dry-run.js +253 -0
  103. package/dist/services/release-evidence.js +138 -0
  104. package/dist/services/release-publish.js +163 -0
  105. package/dist/services/smoke-evidence.js +104 -0
  106. package/dist/services/task-read-model.js +125 -0
  107. package/dist/services/tools-list.js +26 -0
  108. package/dist/services/write-preflight.js +240 -0
  109. package/dist/task/task-capsule.js +121 -0
  110. package/dist/tools/fake-shell.js +56 -0
  111. package/dist/tui/cache.js +341 -0
  112. package/dist/tui/constants.js +44 -0
  113. package/dist/tui/layout.js +140 -0
  114. package/dist/tui/markdown.js +238 -0
  115. package/dist/tui/read-model-worker.js +24 -0
  116. package/dist/tui/read-model.js +502 -0
  117. package/dist/tui/snapshot.js +434 -0
  118. package/dist/tui/state.js +229 -0
  119. package/dist/tui/terminal.js +475 -0
  120. package/dist/tui/theme.js +86 -0
  121. package/package.json +16 -0
@@ -0,0 +1,170 @@
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.EvidenceArtifactPolicyError = void 0;
7
+ exports.appendEvidence = appendEvidence;
8
+ exports.appendEvidenceTextArtifact = appendEvidenceTextArtifact;
9
+ exports.createPublicEvidenceArtifactPolicyReport = createPublicEvidenceArtifactPolicyReport;
10
+ exports.createSessionEvidenceDirs = createSessionEvidenceDirs;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const fs_1 = require("../core/fs");
14
+ const redaction_1 = require("../core/redaction");
15
+ const workspace_1 = require("../core/workspace");
16
+ const private_manifest_1 = require("./private-manifest");
17
+ class EvidenceArtifactPolicyError extends Error {
18
+ code;
19
+ redactionReport;
20
+ constructor(code, message, redactionReport) {
21
+ super(message);
22
+ this.code = code;
23
+ this.redactionReport = redactionReport;
24
+ this.name = 'EvidenceArtifactPolicyError';
25
+ }
26
+ }
27
+ exports.EvidenceArtifactPolicyError = EvidenceArtifactPolicyError;
28
+ function appendEvidence(projectRoot, record) {
29
+ const taskDir = findTaskDir(projectRoot, record.taskId);
30
+ if (!taskDir) {
31
+ throw new Error(`Task capsule not found: ${record.taskId}`);
32
+ }
33
+ const time = new Date().toISOString();
34
+ const visibility = record.visibility ?? 'public';
35
+ const attachedPath = copyPublicEvidenceArtifact({ projectRoot, taskDir, kind: record.kind, sourcePath: record.path, time, visibility });
36
+ return appendEvidenceRecord({ projectRoot, taskDir, time, record, visibility, attachedPath }).markdownPath;
37
+ }
38
+ function appendEvidenceTextArtifact(projectRoot, record, artifact, options = {}) {
39
+ const taskDir = findTaskDir(projectRoot, record.taskId);
40
+ if (!taskDir) {
41
+ throw new Error(`Task capsule not found: ${record.taskId}`);
42
+ }
43
+ const time = new Date().toISOString();
44
+ const visibility = record.visibility ?? 'public';
45
+ const attachedPath = visibility === 'public'
46
+ ? writePublicEvidenceTextArtifact({
47
+ taskDir,
48
+ kind: record.kind,
49
+ time,
50
+ fileName: artifact.fileName,
51
+ content: artifact.content,
52
+ policyOptions: options
53
+ })
54
+ : undefined;
55
+ return appendEvidenceRecord({ projectRoot, taskDir, time, record, visibility, attachedPath });
56
+ }
57
+ function createPublicEvidenceArtifactPolicyReport(content, options = {}) {
58
+ const redaction = (0, redaction_1.createRedactionReport)(content, { patterns: options.redactionPatterns });
59
+ const blocking = (0, redaction_1.hasBlockingRedactionFinding)(redaction, 'high');
60
+ return {
61
+ schemaVersion: 'hadara.evidence_artifact_policy.v1',
62
+ command: 'evidence.artifactPolicy',
63
+ ok: !blocking,
64
+ blocking,
65
+ redaction,
66
+ issues: blocking
67
+ ? [
68
+ {
69
+ severity: 'error',
70
+ code: 'PUBLIC_ARTIFACT_SECRET_DETECTED',
71
+ message: 'Public evidence artifact contains secret-like content; collect it as private evidence or redact the source file first.'
72
+ }
73
+ ]
74
+ : []
75
+ };
76
+ }
77
+ function appendEvidenceIndex(taskDir, record) {
78
+ node_fs_1.default.appendFileSync(node_path_1.default.join(taskDir, 'evidence.jsonl'), `${JSON.stringify(record)}\n`, 'utf8');
79
+ }
80
+ function appendEvidenceRecord(input) {
81
+ const summary = (0, redaction_1.redactSecrets)(input.record.summary.replace(/\|/g, '/'));
82
+ const markdownPath = node_path_1.default.join(input.taskDir, 'EVIDENCE.md');
83
+ const rowSummary = input.visibility === 'private' || !input.attachedPath ? summary : `${summary} (${input.attachedPath})`;
84
+ const row = `| ${input.time} | ${input.record.kind} | ${rowSummary} | ${input.record.result} |\n`;
85
+ if (!node_fs_1.default.existsSync(markdownPath)) {
86
+ node_fs_1.default.writeFileSync(markdownPath, '# Evidence\n\n| Time | Kind | Summary | Result |\n|---|---|---|---|\n', 'utf8');
87
+ }
88
+ node_fs_1.default.appendFileSync(markdownPath, row, 'utf8');
89
+ const evidence = {
90
+ schemaVersion: 'hadara.evidence.v1',
91
+ time: input.time,
92
+ taskId: input.record.taskId,
93
+ kind: input.record.kind,
94
+ summary,
95
+ result: input.record.result,
96
+ visibility: input.visibility,
97
+ ...(input.visibility === 'public' && input.attachedPath ? { evidencePath: input.attachedPath } : {})
98
+ };
99
+ appendEvidenceIndex(input.taskDir, evidence);
100
+ if (input.visibility === 'private') {
101
+ (0, private_manifest_1.writePrivateEvidenceManifest)({
102
+ projectRoot: input.projectRoot,
103
+ taskId: input.record.taskId,
104
+ kind: input.record.kind,
105
+ summary,
106
+ result: input.record.result,
107
+ sourcePath: input.record.path,
108
+ time: input.time
109
+ });
110
+ }
111
+ return { markdownPath, evidence };
112
+ }
113
+ function copyPublicEvidenceArtifact(input) {
114
+ if (!input.sourcePath || input.visibility === 'private')
115
+ return undefined;
116
+ const sourceFile = (0, workspace_1.resolveProjectFile)(input.projectRoot, input.sourcePath);
117
+ const artifactText = readPublicTextArtifact(sourceFile.absolutePath);
118
+ const policy = createPublicEvidenceArtifactPolicyReport(artifactText);
119
+ if (policy.blocking) {
120
+ throw new EvidenceArtifactPolicyError('PUBLIC_ARTIFACT_SECRET_DETECTED', 'Public evidence artifact contains secret-like content; collect it as private evidence or redact the source file first.', policy.redaction);
121
+ }
122
+ const artifactsDir = node_path_1.default.join(input.taskDir, 'artifacts', input.kind);
123
+ (0, fs_1.ensureDir)(artifactsDir);
124
+ const targetPath = node_path_1.default.join(artifactsDir, `${safeFilePart(input.time)}-${safeFilePart(node_path_1.default.basename(sourceFile.absolutePath))}`);
125
+ node_fs_1.default.writeFileSync(targetPath, artifactText, 'utf8');
126
+ return toPortablePath(node_path_1.default.relative(input.taskDir, targetPath));
127
+ }
128
+ function writePublicEvidenceTextArtifact(input) {
129
+ const policy = createPublicEvidenceArtifactPolicyReport(input.content, input.policyOptions);
130
+ if (policy.blocking) {
131
+ throw new EvidenceArtifactPolicyError('PUBLIC_ARTIFACT_SECRET_DETECTED', 'Public evidence artifact contains secret-like content; collect it as private evidence or redact the source file first.', policy.redaction);
132
+ }
133
+ const artifactsDir = node_path_1.default.join(input.taskDir, 'artifacts', input.kind);
134
+ (0, fs_1.ensureDir)(artifactsDir);
135
+ const targetPath = node_path_1.default.join(artifactsDir, `${safeFilePart(input.time)}-${safeFilePart(input.fileName)}`);
136
+ node_fs_1.default.writeFileSync(targetPath, input.content, 'utf8');
137
+ return toPortablePath(node_path_1.default.relative(input.taskDir, targetPath));
138
+ }
139
+ function readPublicTextArtifact(filePath) {
140
+ const content = node_fs_1.default.readFileSync(filePath);
141
+ if (!isTextBuffer(content)) {
142
+ throw new EvidenceArtifactPolicyError('PUBLIC_ARTIFACT_BINARY_REJECTED', 'Public evidence artifacts must be UTF-8 text; collect binary evidence as private evidence until binary policy is implemented.');
143
+ }
144
+ return content.toString('utf8');
145
+ }
146
+ function isTextBuffer(content) {
147
+ if (content.includes(0))
148
+ return false;
149
+ return !content.toString('utf8').includes('\uFFFD');
150
+ }
151
+ function findTaskDir(projectRoot, taskId) {
152
+ const tasksDir = node_path_1.default.join(projectRoot, 'tasks');
153
+ if (!node_fs_1.default.existsSync(tasksDir))
154
+ return null;
155
+ const entry = node_fs_1.default.readdirSync(tasksDir).find((name) => name.startsWith(`${taskId}-`));
156
+ return entry ? node_path_1.default.join(tasksDir, entry) : null;
157
+ }
158
+ function createSessionEvidenceDirs(dataRoot, sessionId) {
159
+ const evidenceDir = node_path_1.default.join(dataRoot, 'sessions', sessionId, 'evidence');
160
+ for (const child of ['command-logs', 'test-results', 'diff-summary', 'screenshots', 'release-smoke']) {
161
+ (0, fs_1.ensureDir)(node_path_1.default.join(evidenceDir, child));
162
+ }
163
+ return evidenceDir;
164
+ }
165
+ function safeFilePart(value) {
166
+ return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'artifact';
167
+ }
168
+ function toPortablePath(value) {
169
+ return value.split(node_path_1.default.sep).join('/');
170
+ }
@@ -0,0 +1,101 @@
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.writePrivateEvidenceManifest = writePrivateEvidenceManifest;
7
+ exports.listPrivateEvidenceManifests = listPrivateEvidenceManifests;
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const audit_1 = require("../core/audit");
12
+ const fs_1 = require("../core/fs");
13
+ const redaction_1 = require("../core/redaction");
14
+ const paths_1 = require("../core/paths");
15
+ const workspace_1 = require("../core/workspace");
16
+ function writePrivateEvidenceManifest(input) {
17
+ const source = readPrivateEvidenceSource(input.projectRoot, input.sourcePath);
18
+ if (!source)
19
+ return null;
20
+ const paths = (0, paths_1.resolveHadaraPaths)({ projectRoot: input.projectRoot });
21
+ const evidenceId = createEvidenceId(input.time);
22
+ const privateDir = node_path_1.default.join(paths.dataRoot, 'private-evidence', input.taskId);
23
+ (0, fs_1.ensureDir)(privateDir);
24
+ const artifactPath = node_path_1.default.join(privateDir, `${evidenceId}.bin`);
25
+ node_fs_1.default.writeFileSync(artifactPath, source);
26
+ const manifest = {
27
+ schemaVersion: 'hadara.privateEvidence.v1',
28
+ taskId: input.taskId,
29
+ evidenceId,
30
+ kind: input.kind,
31
+ summary: (0, redaction_1.redactSecrets)(input.summary),
32
+ result: input.result,
33
+ storage: {
34
+ kind: 'portable-store',
35
+ relativePath: toPortablePath(node_path_1.default.relative(paths.portableRoot, artifactPath)),
36
+ encrypted: false,
37
+ hash: `sha256:${node_crypto_1.default.createHash('sha256').update(source).digest('hex')}`,
38
+ byteLength: source.byteLength
39
+ },
40
+ createdAt: input.time,
41
+ retention: {
42
+ policy: 'local-only',
43
+ includeInContextExport: false
44
+ },
45
+ encryption: {
46
+ status: 'deferred',
47
+ reason: 'Private evidence encryption is deferred; content remains in the private portable store and is excluded from committed project context.'
48
+ }
49
+ };
50
+ (0, fs_1.writeJsonl)(node_path_1.default.join(privateDir, 'manifest.jsonl'), manifest);
51
+ (0, audit_1.writeAuditEvent)(paths.auditDir, {
52
+ actor: 'system',
53
+ task_id: input.taskId,
54
+ event_type: 'evidence.private_manifest.created',
55
+ risk: 'medium',
56
+ summary: `Private evidence manifest created for ${input.taskId}`,
57
+ payload: {
58
+ evidenceId,
59
+ kind: input.kind,
60
+ result: input.result,
61
+ hash: manifest.storage.hash,
62
+ byteLength: manifest.storage.byteLength,
63
+ includeInContextExport: false,
64
+ encrypted: false
65
+ }
66
+ });
67
+ return manifest;
68
+ }
69
+ function listPrivateEvidenceManifests(projectRoot, taskId) {
70
+ const paths = (0, paths_1.resolveHadaraPaths)({ projectRoot });
71
+ const manifestPath = node_path_1.default.join(paths.dataRoot, 'private-evidence', taskId, 'manifest.jsonl');
72
+ if (!node_fs_1.default.existsSync(manifestPath))
73
+ return [];
74
+ return node_fs_1.default
75
+ .readFileSync(manifestPath, 'utf8')
76
+ .split(/\r?\n/)
77
+ .filter(Boolean)
78
+ .map((line) => JSON.parse(line));
79
+ }
80
+ function readPrivateEvidenceSource(projectRoot, sourcePath) {
81
+ if (!sourcePath)
82
+ return null;
83
+ try {
84
+ const sourceFile = (0, workspace_1.resolveProjectFile)(projectRoot, sourcePath);
85
+ return node_fs_1.default.readFileSync(sourceFile.absolutePath);
86
+ }
87
+ catch (error) {
88
+ if (error instanceof workspace_1.WorkspaceFileError)
89
+ return null;
90
+ return null;
91
+ }
92
+ }
93
+ function createEvidenceId(time) {
94
+ return `ev_${safeFilePart(time)}_${node_crypto_1.default.randomBytes(4).toString('hex')}`;
95
+ }
96
+ function safeFilePart(value) {
97
+ return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'evidence';
98
+ }
99
+ function toPortablePath(value) {
100
+ return value.split(node_path_1.default.sep).join('/');
101
+ }
@@ -0,0 +1,49 @@
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.updateHandoff = updateHandoff;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const fs_1 = require("../core/fs");
10
+ function updateHandoff(input) {
11
+ const docsDir = node_path_1.default.join(input.projectRoot, 'docs');
12
+ (0, fs_1.ensureDir)(docsDir);
13
+ const filePath = node_path_1.default.join(docsDir, 'AGENT_HANDOFF.md');
14
+ const content = `# AGENT_HANDOFF
15
+
16
+ ## Current Branch
17
+
18
+ TBD. Run \`git branch --show-current\`.
19
+
20
+ ## Last Completed
21
+
22
+ ${input.summary ?? 'TBD.'}
23
+
24
+ ## In Progress
25
+
26
+ ${input.taskId ?? 'No active task recorded.'}
27
+
28
+ ## Do Not Change Without Updating Tests
29
+
30
+ - ProviderClient contract
31
+ - Policy decision matrix
32
+ - Task Capsule file contract
33
+ - Portable/project store boundary
34
+
35
+ ## Known Problems
36
+
37
+ TBD.
38
+
39
+ ## Next Recommended Step
40
+
41
+ ${input.nextStep ?? 'Read PROJECT_STATE.md, TASK_BOARD.md, and the active Task Capsule before continuing.'}
42
+
43
+ ## Evidence
44
+
45
+ Attach test logs, command outputs, and diff summaries under the active Task Capsule.
46
+ `;
47
+ node_fs_1.default.writeFileSync(filePath, content, 'utf8');
48
+ return filePath;
49
+ }
@@ -0,0 +1,200 @@
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.replayScenario = replayScenario;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const workspace_1 = require("../core/workspace");
9
+ const scripted_provider_1 = require("../providers/scripted-provider");
10
+ async function replayScenario(projectRoot, scenarioPath) {
11
+ let scenario = scenarioPath;
12
+ const issues = [];
13
+ const steps = [];
14
+ let resolvedScenarioPath;
15
+ try {
16
+ const scenarioFile = (0, workspace_1.resolveProjectFile)(projectRoot, scenarioPath);
17
+ resolvedScenarioPath = scenarioFile.absolutePath;
18
+ scenario = scenarioFile.relativePath;
19
+ }
20
+ catch (error) {
21
+ return {
22
+ schemaVersion: 'hadara.harness.replay.v1',
23
+ command: 'harness.replay',
24
+ ok: false,
25
+ scenario,
26
+ eventsRead: 0,
27
+ steps,
28
+ issues: [
29
+ {
30
+ severity: 'error',
31
+ code: error instanceof workspace_1.WorkspaceFileError && error.code !== 'WORKSPACE_FILE_NOT_FOUND' ? error.code : 'SCENARIO_NOT_FOUND',
32
+ message: error instanceof workspace_1.WorkspaceFileError && error.code !== 'WORKSPACE_FILE_NOT_FOUND'
33
+ ? error.message
34
+ : `Replay scenario not found: ${scenario}`
35
+ }
36
+ ]
37
+ };
38
+ }
39
+ if (!node_fs_1.default.existsSync(resolvedScenarioPath)) {
40
+ return {
41
+ schemaVersion: 'hadara.harness.replay.v1',
42
+ command: 'harness.replay',
43
+ ok: false,
44
+ scenario,
45
+ eventsRead: 0,
46
+ steps,
47
+ issues: [
48
+ {
49
+ severity: 'error',
50
+ code: 'SCENARIO_NOT_FOUND',
51
+ message: `Replay scenario not found: ${scenario}`
52
+ }
53
+ ]
54
+ };
55
+ }
56
+ const lines = node_fs_1.default
57
+ .readFileSync(resolvedScenarioPath, 'utf8')
58
+ .split(/\r?\n/)
59
+ .map((line, index) => ({ line: index + 1, text: line.trim() }))
60
+ .filter((line) => line.text.length > 0);
61
+ const events = parseEvents(lines, issues);
62
+ if (!issues.some((issue) => issue.severity === 'error')) {
63
+ await executeEvents(events, steps, issues);
64
+ }
65
+ return {
66
+ schemaVersion: 'hadara.harness.replay.v1',
67
+ command: 'harness.replay',
68
+ ok: !issues.some((issue) => issue.severity === 'error'),
69
+ scenario,
70
+ eventsRead: lines.length,
71
+ steps,
72
+ issues
73
+ };
74
+ }
75
+ function parseEvents(lines, issues) {
76
+ const events = [];
77
+ for (const line of lines) {
78
+ try {
79
+ const parsed = JSON.parse(line.text);
80
+ const event = normalizeEvent(parsed, line.line, issues);
81
+ if (event)
82
+ events.push({ line: line.line, event });
83
+ }
84
+ catch {
85
+ issues.push({
86
+ severity: 'error',
87
+ code: 'SCENARIO_JSON_INVALID',
88
+ message: `Scenario line ${line.line} is not valid JSON.`,
89
+ line: line.line
90
+ });
91
+ }
92
+ }
93
+ return events;
94
+ }
95
+ function normalizeEvent(parsed, line, issues) {
96
+ if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) {
97
+ issues.push({
98
+ severity: 'error',
99
+ code: 'SCENARIO_EVENT_INVALID',
100
+ message: `Scenario line ${line} must be an object with a type field.`,
101
+ line
102
+ });
103
+ return null;
104
+ }
105
+ const record = parsed;
106
+ if (record.type === 'user' && typeof record.content === 'string') {
107
+ return { type: 'user', content: record.content };
108
+ }
109
+ if (record.type === 'assistant_response' && typeof record.content === 'string') {
110
+ return {
111
+ type: 'assistant_response',
112
+ content: record.content,
113
+ ...(typeof record.match === 'string' ? { match: record.match } : {}),
114
+ ...(isFinishReason(record.finishReason) ? { finishReason: record.finishReason } : {})
115
+ };
116
+ }
117
+ if (record.type === 'expect_final' && typeof record.content === 'string') {
118
+ return { type: 'expect_final', content: record.content };
119
+ }
120
+ issues.push({
121
+ severity: 'error',
122
+ code: 'SCENARIO_EVENT_INVALID',
123
+ message: `Scenario line ${line} has an unsupported event shape.`,
124
+ line
125
+ });
126
+ return null;
127
+ }
128
+ async function executeEvents(events, steps, issues) {
129
+ let pendingUser = null;
130
+ let lastAssistantContent = null;
131
+ let sawExpectation = false;
132
+ for (const item of events) {
133
+ if (item.event.type === 'user') {
134
+ pendingUser = { line: item.line, content: item.event.content };
135
+ steps.push({ line: item.line, type: item.event.type, ok: true, summary: 'Accepted user prompt.' });
136
+ continue;
137
+ }
138
+ if (item.event.type === 'assistant_response') {
139
+ if (!pendingUser) {
140
+ issues.push({
141
+ severity: 'error',
142
+ code: 'REPLAY_ORDER_INVALID',
143
+ message: 'assistant_response requires a preceding user event.',
144
+ line: item.line
145
+ });
146
+ steps.push({ line: item.line, type: item.event.type, ok: false, summary: 'Missing preceding user event.' });
147
+ continue;
148
+ }
149
+ const provider = new scripted_provider_1.ScriptedProvider([toScriptedProviderStep(item.event)]);
150
+ const response = await provider.chat({ messages: [{ role: 'user', content: pendingUser.content }] });
151
+ lastAssistantContent = response.content;
152
+ pendingUser = null;
153
+ steps.push({
154
+ line: item.line,
155
+ type: item.event.type,
156
+ ok: true,
157
+ summary: `ScriptedProvider returned ${response.finishReason}.`
158
+ });
159
+ continue;
160
+ }
161
+ sawExpectation = true;
162
+ if (lastAssistantContent !== item.event.content) {
163
+ issues.push({
164
+ severity: 'error',
165
+ code: 'REPLAY_EXPECTATION_FAILED',
166
+ message: 'Final assistant content did not match expect_final.',
167
+ line: item.line
168
+ });
169
+ steps.push({ line: item.line, type: item.event.type, ok: false, summary: 'Final content mismatch.' });
170
+ }
171
+ else {
172
+ steps.push({ line: item.line, type: item.event.type, ok: true, summary: 'Final content matched.' });
173
+ }
174
+ }
175
+ if (pendingUser) {
176
+ issues.push({
177
+ severity: 'error',
178
+ code: 'REPLAY_INCOMPLETE',
179
+ message: 'Scenario ended with a user event that had no assistant_response.',
180
+ line: pendingUser.line
181
+ });
182
+ }
183
+ if (!sawExpectation) {
184
+ issues.push({
185
+ severity: 'error',
186
+ code: 'REPLAY_EXPECTATION_MISSING',
187
+ message: 'Scenario must include an expect_final event.'
188
+ });
189
+ }
190
+ }
191
+ function toScriptedProviderStep(event) {
192
+ return {
193
+ response: event.content,
194
+ ...(event.match ? { match: event.match } : {}),
195
+ ...(event.finishReason ? { finishReason: event.finishReason } : {})
196
+ };
197
+ }
198
+ function isFinishReason(value) {
199
+ return value === 'stop' || value === 'length' || value === 'tool_call' || value === 'error';
200
+ }