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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/agent/evidence.js +50 -0
- package/dist/agent/loop.js +124 -0
- package/dist/cli/args.js +70 -0
- package/dist/cli/dashboard.js +185 -0
- package/dist/cli/debt.js +41 -0
- package/dist/cli/doctor.js +68 -0
- package/dist/cli/errors.js +58 -0
- package/dist/cli/evidence-json.js +75 -0
- package/dist/cli/evidence.js +80 -0
- package/dist/cli/handoff.js +16 -0
- package/dist/cli/harness.js +57 -0
- package/dist/cli/hermes-json.js +31 -0
- package/dist/cli/hermes.js +28 -0
- package/dist/cli/init.js +142 -0
- package/dist/cli/install.js +34 -0
- package/dist/cli/main.js +216 -0
- package/dist/cli/mcp.js +15 -0
- package/dist/cli/package-smoke.js +37 -0
- package/dist/cli/policy-json.js +22 -0
- package/dist/cli/policy.js +43 -0
- package/dist/cli/release-artifact.js +47 -0
- package/dist/cli/release-dry-run.js +24 -0
- package/dist/cli/release-gate.js +28 -0
- package/dist/cli/release-publish.js +41 -0
- package/dist/cli/run-scaffold.js +68 -0
- package/dist/cli/run-state.js +41 -0
- package/dist/cli/run.js +191 -0
- package/dist/cli/smoke.js +58 -0
- package/dist/cli/status-json.js +6 -0
- package/dist/cli/status.js +26 -0
- package/dist/cli/task-json.js +8 -0
- package/dist/cli/task.js +64 -0
- package/dist/cli/tools.js +25 -0
- package/dist/cli/tui.js +72 -0
- package/dist/cli/write-preflight.js +27 -0
- package/dist/core/audit.js +41 -0
- package/dist/core/events.js +63 -0
- package/dist/core/fs.js +44 -0
- package/dist/core/paths.js +59 -0
- package/dist/core/redaction.js +178 -0
- package/dist/core/schema.js +253 -0
- package/dist/core/workspace.js +47 -0
- package/dist/evidence/evidence.js +170 -0
- package/dist/evidence/private-manifest.js +101 -0
- package/dist/handoff/handoff.js +49 -0
- package/dist/harness/replay.js +200 -0
- package/dist/harness/validate.js +465 -0
- package/dist/hermes/context-export.js +104 -0
- package/dist/index.js +29 -0
- package/dist/mcp/server.js +104 -0
- package/dist/mcp/tool-dispatch.js +159 -0
- package/dist/mcp/tool-registry.js +150 -0
- package/dist/mcp/tool-schemas.js +18 -0
- package/dist/policy/command-risk.js +39 -0
- package/dist/policy/permission-matrix.js +42 -0
- package/dist/policy/policy.js +20 -0
- package/dist/policy/preflight.js +47 -0
- package/dist/policy/presets.js +24 -0
- package/dist/policy/tokenizer.js +53 -0
- package/dist/providers/fallback-executor.js +46 -0
- package/dist/providers/mock-provider.js +49 -0
- package/dist/providers/provider-contract.js +2 -0
- package/dist/providers/provider-preparation.js +220 -0
- package/dist/providers/scripted-provider.js +69 -0
- package/dist/schemas/active-run-projection.schema.json +73 -0
- package/dist/schemas/active-run-resume.schema.json +68 -0
- package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
- package/dist/schemas/context-export.schema.json +35 -0
- package/dist/schemas/event.schema.json +17 -0
- package/dist/schemas/evidence-list.schema.json +49 -0
- package/dist/schemas/feature-smoke.schema.json +67 -0
- package/dist/schemas/install-plan.schema.json +93 -0
- package/dist/schemas/package-smoke.schema.json +130 -0
- package/dist/schemas/private-evidence.schema.json +48 -0
- package/dist/schemas/provider-call.schema.json +42 -0
- package/dist/schemas/provider-config.schema.json +43 -0
- package/dist/schemas/release-artifact-manifest.schema.json +55 -0
- package/dist/schemas/release-artifact.schema.json +140 -0
- package/dist/schemas/release-dry-run.schema.json +141 -0
- package/dist/schemas/release-gate.schema.json +42 -0
- package/dist/schemas/release-publish.schema.json +114 -0
- package/dist/schemas/schema-index.json +145 -0
- package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
- package/dist/schemas/tools-list.schema.json +78 -0
- package/dist/schemas/write-preflight.schema.json +47 -0
- package/dist/services/active-run-state.js +215 -0
- package/dist/services/capability-registry.js +540 -0
- package/dist/services/clean-checkout-smoke.js +393 -0
- package/dist/services/evidence-list.js +136 -0
- package/dist/services/feature-smoke.js +155 -0
- package/dist/services/harness-service.js +7 -0
- package/dist/services/install-plan.js +233 -0
- package/dist/services/operational-debt.js +767 -0
- package/dist/services/operations-status-service.js +195 -0
- package/dist/services/package-smoke.js +676 -0
- package/dist/services/policy-service.js +25 -0
- package/dist/services/project-read-model.js +101 -0
- package/dist/services/release-artifact-evidence.js +77 -0
- package/dist/services/release-artifact.js +351 -0
- package/dist/services/release-dry-run.js +253 -0
- package/dist/services/release-evidence.js +138 -0
- package/dist/services/release-publish.js +163 -0
- package/dist/services/smoke-evidence.js +104 -0
- package/dist/services/task-read-model.js +125 -0
- package/dist/services/tools-list.js +26 -0
- package/dist/services/write-preflight.js +240 -0
- package/dist/task/task-capsule.js +121 -0
- package/dist/tools/fake-shell.js +56 -0
- package/dist/tui/cache.js +341 -0
- package/dist/tui/constants.js +44 -0
- package/dist/tui/layout.js +140 -0
- package/dist/tui/markdown.js +238 -0
- package/dist/tui/read-model-worker.js +24 -0
- package/dist/tui/read-model.js +502 -0
- package/dist/tui/snapshot.js +434 -0
- package/dist/tui/state.js +229 -0
- package/dist/tui/terminal.js +475 -0
- package/dist/tui/theme.js +86 -0
- 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
|
+
}
|