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,465 @@
|
|
|
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.validateTaskCapsule = validateTaskCapsule;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
10
|
+
const REQUIRED_TASK_FILES = [
|
|
11
|
+
'TASK.md',
|
|
12
|
+
'PLAN.md',
|
|
13
|
+
'CONTEXT.md',
|
|
14
|
+
'FILES.md',
|
|
15
|
+
'ACCEPTANCE.md',
|
|
16
|
+
'TESTS.md',
|
|
17
|
+
'RISKS.md',
|
|
18
|
+
'DECISIONS.md',
|
|
19
|
+
'EVIDENCE.md',
|
|
20
|
+
'evidence.jsonl',
|
|
21
|
+
'HANDOFF.md'
|
|
22
|
+
];
|
|
23
|
+
const EVIDENCE_KINDS = new Set(['test-log', 'command-log', 'diff-summary', 'screenshot', 'note']);
|
|
24
|
+
const EVIDENCE_RESULTS = new Set(['passed', 'failed', 'blocked', 'unknown']);
|
|
25
|
+
const EVIDENCE_VISIBILITIES = new Set(['public', 'private']);
|
|
26
|
+
function validateTaskCapsule(projectRoot, taskId, options = {}) {
|
|
27
|
+
const level = options.level ?? 'draft';
|
|
28
|
+
const task = findTask(projectRoot, taskId);
|
|
29
|
+
if (!task) {
|
|
30
|
+
return {
|
|
31
|
+
schemaVersion: 'hadara.harness.validate.v1',
|
|
32
|
+
command: 'harness.validate',
|
|
33
|
+
ok: false,
|
|
34
|
+
level,
|
|
35
|
+
task: { id: taskId, title: '', capsule: '' },
|
|
36
|
+
checkedFiles: [],
|
|
37
|
+
issues: [
|
|
38
|
+
{
|
|
39
|
+
severity: 'error',
|
|
40
|
+
code: 'TASK_NOT_FOUND',
|
|
41
|
+
message: `Task Capsule not found: ${taskId}`
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const issues = [];
|
|
47
|
+
const checkedFiles = [];
|
|
48
|
+
for (const fileName of REQUIRED_TASK_FILES) {
|
|
49
|
+
const filePath = node_path_1.default.join(task.dir, fileName);
|
|
50
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, filePath));
|
|
51
|
+
checkedFiles.push(relativePath);
|
|
52
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
53
|
+
issues.push({
|
|
54
|
+
severity: 'error',
|
|
55
|
+
code: 'MISSING_TASK_FILE',
|
|
56
|
+
message: `Required Task Capsule file is missing: ${fileName}`,
|
|
57
|
+
path: relativePath
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
validateTaskMarkdown(projectRoot, task, issues);
|
|
62
|
+
validateCapsuleFormatMarkdown(projectRoot, task, issues);
|
|
63
|
+
validateEvidenceMarkdown(projectRoot, task, issues);
|
|
64
|
+
validateEvidenceIndex(projectRoot, task, issues);
|
|
65
|
+
if (level === 'done') {
|
|
66
|
+
validateDoneLevel(projectRoot, task, issues, checkedFiles);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
schemaVersion: 'hadara.harness.validate.v1',
|
|
70
|
+
command: 'harness.validate',
|
|
71
|
+
ok: !issues.some((issue) => issue.severity === 'error'),
|
|
72
|
+
level,
|
|
73
|
+
task: {
|
|
74
|
+
id: task.id,
|
|
75
|
+
title: task.title,
|
|
76
|
+
capsule: toPortablePath(node_path_1.default.relative(projectRoot, task.dir))
|
|
77
|
+
},
|
|
78
|
+
checkedFiles,
|
|
79
|
+
issues
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function findTask(projectRoot, taskId) {
|
|
83
|
+
return (0, task_capsule_1.listTaskCapsules)(projectRoot).find((task) => task.id === taskId);
|
|
84
|
+
}
|
|
85
|
+
function validateTaskMarkdown(projectRoot, task, issues) {
|
|
86
|
+
const taskPath = node_path_1.default.join(task.dir, 'TASK.md');
|
|
87
|
+
if (!node_fs_1.default.existsSync(taskPath))
|
|
88
|
+
return;
|
|
89
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, taskPath));
|
|
90
|
+
const content = node_fs_1.default.readFileSync(taskPath, 'utf8');
|
|
91
|
+
for (const heading of ['## Goal', '## Scope', '## Out of Scope', '## Status']) {
|
|
92
|
+
if (!content.includes(heading)) {
|
|
93
|
+
issues.push({
|
|
94
|
+
severity: 'error',
|
|
95
|
+
code: 'TASK_SECTION_MISSING',
|
|
96
|
+
message: `TASK.md is missing required section: ${heading}`,
|
|
97
|
+
path: relativePath
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function validateCapsuleFormatMarkdown(projectRoot, task, issues) {
|
|
103
|
+
validateMarkdownFile(projectRoot, task, issues, 'ACCEPTANCE.md', [
|
|
104
|
+
{ code: 'ACCEPTANCE_HEADING_INVALID', anyText: ['# Acceptance Criteria'] },
|
|
105
|
+
{ code: 'ACCEPTANCE_CHECKLIST_MISSING', anyText: ['- [ ]', '- [x]'] }
|
|
106
|
+
]);
|
|
107
|
+
validateMarkdownFile(projectRoot, task, issues, 'FILES.md', [
|
|
108
|
+
{ code: 'FILES_TABLE_INVALID', anyText: ['| Path | Action | Reason |'] },
|
|
109
|
+
{ code: 'FILES_TABLE_INVALID', anyText: ['|---|---|---|'] }
|
|
110
|
+
]);
|
|
111
|
+
validateMarkdownFile(projectRoot, task, issues, 'TESTS.md', [
|
|
112
|
+
{ code: 'TESTS_SECTION_MISSING', anyText: ['## Required'] },
|
|
113
|
+
{ code: 'TESTS_SECTION_MISSING', anyText: ['## Optional'] }
|
|
114
|
+
]);
|
|
115
|
+
validateMarkdownFile(projectRoot, task, issues, 'RISKS.md', [
|
|
116
|
+
{ code: 'RISKS_TABLE_INVALID', anyText: ['| Risk | Mitigation |'] },
|
|
117
|
+
{ code: 'RISKS_TABLE_INVALID', anyText: ['|---|---|'] }
|
|
118
|
+
]);
|
|
119
|
+
validateMarkdownFile(projectRoot, task, issues, 'HANDOFF.md', [
|
|
120
|
+
{ code: 'HANDOFF_SECTION_MISSING', anyText: ['## Last Completed'] },
|
|
121
|
+
{ code: 'HANDOFF_SECTION_MISSING', anyText: ['## Next Recommended Step'] }
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
function validateMarkdownFile(projectRoot, task, issues, fileName, checks) {
|
|
125
|
+
const filePath = node_path_1.default.join(task.dir, fileName);
|
|
126
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
127
|
+
return;
|
|
128
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, filePath));
|
|
129
|
+
const content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
130
|
+
for (const check of checks) {
|
|
131
|
+
if (!check.anyText.some((text) => content.includes(text))) {
|
|
132
|
+
issues.push({
|
|
133
|
+
severity: 'error',
|
|
134
|
+
code: check.code,
|
|
135
|
+
message: `${fileName} is missing standard Task Capsule format marker: ${check.anyText.join(' or ')}`,
|
|
136
|
+
path: relativePath
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function validateEvidenceMarkdown(projectRoot, task, issues) {
|
|
142
|
+
const evidencePath = node_path_1.default.join(task.dir, 'EVIDENCE.md');
|
|
143
|
+
if (!node_fs_1.default.existsSync(evidencePath))
|
|
144
|
+
return;
|
|
145
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, evidencePath));
|
|
146
|
+
const lines = node_fs_1.default.readFileSync(evidencePath, 'utf8').split(/\r?\n/);
|
|
147
|
+
const headerIndex = lines.findIndex((line) => line.trim() === '| Time | Kind | Summary | Result |');
|
|
148
|
+
if (headerIndex < 0 || lines[headerIndex + 1]?.trim() !== '|---|---|---|---|') {
|
|
149
|
+
issues.push({
|
|
150
|
+
severity: 'error',
|
|
151
|
+
code: 'EVIDENCE_TABLE_INVALID',
|
|
152
|
+
message: 'EVIDENCE.md must contain the standard evidence table header.',
|
|
153
|
+
path: relativePath
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function validateEvidenceIndex(projectRoot, task, issues) {
|
|
158
|
+
const indexPath = node_path_1.default.join(task.dir, 'evidence.jsonl');
|
|
159
|
+
if (!node_fs_1.default.existsSync(indexPath))
|
|
160
|
+
return;
|
|
161
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, indexPath));
|
|
162
|
+
const content = node_fs_1.default.readFileSync(indexPath, 'utf8').trim();
|
|
163
|
+
if (!content)
|
|
164
|
+
return;
|
|
165
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
166
|
+
try {
|
|
167
|
+
const record = JSON.parse(line);
|
|
168
|
+
if (record.schemaVersion !== 'hadara.evidence.v1') {
|
|
169
|
+
issues.push({
|
|
170
|
+
severity: 'error',
|
|
171
|
+
code: 'EVIDENCE_INDEX_SCHEMA_INVALID',
|
|
172
|
+
message: `evidence.jsonl line ${index + 1} has an unsupported schemaVersion.`,
|
|
173
|
+
path: relativePath
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (record.taskId !== task.id || typeof record.kind !== 'string' || typeof record.result !== 'string') {
|
|
177
|
+
issues.push({
|
|
178
|
+
severity: 'error',
|
|
179
|
+
code: 'EVIDENCE_INDEX_RECORD_INVALID',
|
|
180
|
+
message: `evidence.jsonl line ${index + 1} is missing required evidence fields.`,
|
|
181
|
+
path: relativePath
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (typeof record.time !== 'string' || !record.time.trim()) {
|
|
185
|
+
issues.push({
|
|
186
|
+
severity: 'error',
|
|
187
|
+
code: 'EVIDENCE_INDEX_TIME_MISSING',
|
|
188
|
+
message: `evidence.jsonl line ${index + 1} is missing required time.`,
|
|
189
|
+
path: relativePath
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (typeof record.summary !== 'string' || !record.summary.trim()) {
|
|
193
|
+
issues.push({
|
|
194
|
+
severity: 'error',
|
|
195
|
+
code: 'EVIDENCE_INDEX_SUMMARY_MISSING',
|
|
196
|
+
message: `evidence.jsonl line ${index + 1} is missing required summary.`,
|
|
197
|
+
path: relativePath
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (typeof record.visibility !== 'string') {
|
|
201
|
+
issues.push({
|
|
202
|
+
severity: 'error',
|
|
203
|
+
code: 'EVIDENCE_INDEX_VISIBILITY_MISSING',
|
|
204
|
+
message: `evidence.jsonl line ${index + 1} is missing required visibility.`,
|
|
205
|
+
path: relativePath
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if ((typeof record.kind === 'string' && !EVIDENCE_KINDS.has(record.kind)) ||
|
|
209
|
+
(typeof record.result === 'string' && !EVIDENCE_RESULTS.has(record.result)) ||
|
|
210
|
+
(typeof record.visibility === 'string' && !EVIDENCE_VISIBILITIES.has(record.visibility))) {
|
|
211
|
+
issues.push({
|
|
212
|
+
severity: 'error',
|
|
213
|
+
code: 'EVIDENCE_INDEX_ENUM_INVALID',
|
|
214
|
+
message: `evidence.jsonl line ${index + 1} has an unsupported evidence enum value.`,
|
|
215
|
+
path: relativePath
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
issues.push({
|
|
221
|
+
severity: 'error',
|
|
222
|
+
code: 'EVIDENCE_INDEX_JSON_INVALID',
|
|
223
|
+
message: `evidence.jsonl line ${index + 1} is not valid JSON.`,
|
|
224
|
+
path: relativePath
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function validateDoneLevel(projectRoot, task, issues, checkedFiles) {
|
|
230
|
+
validateTaskStatusDone(projectRoot, task, issues);
|
|
231
|
+
validateDoneLevelScaffoldContent(projectRoot, task, issues);
|
|
232
|
+
validateAcceptanceDone(projectRoot, task, issues);
|
|
233
|
+
validateEvidenceMarkdownSingleTable(projectRoot, task, issues);
|
|
234
|
+
validateEvidenceIndexHasRecords(projectRoot, task, issues);
|
|
235
|
+
validateHandoffDone(projectRoot, task, issues);
|
|
236
|
+
validateTaskBoardDone(projectRoot, task, issues, checkedFiles);
|
|
237
|
+
}
|
|
238
|
+
function validateTaskStatusDone(projectRoot, task, issues) {
|
|
239
|
+
const taskPath = node_path_1.default.join(task.dir, 'TASK.md');
|
|
240
|
+
if (!node_fs_1.default.existsSync(taskPath))
|
|
241
|
+
return;
|
|
242
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, taskPath));
|
|
243
|
+
const status = readSectionBody(taskPath, '## Status');
|
|
244
|
+
if (!/^Done\b/i.test(status.trim())) {
|
|
245
|
+
issues.push({
|
|
246
|
+
severity: 'error',
|
|
247
|
+
code: 'TASK_STATUS_NOT_DONE',
|
|
248
|
+
message: 'Done-level validation requires TASK.md status to be Done.',
|
|
249
|
+
path: relativePath
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function validateAcceptanceDone(projectRoot, task, issues) {
|
|
254
|
+
const acceptancePath = node_path_1.default.join(task.dir, 'ACCEPTANCE.md');
|
|
255
|
+
if (!node_fs_1.default.existsSync(acceptancePath))
|
|
256
|
+
return;
|
|
257
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, acceptancePath));
|
|
258
|
+
const checklistLines = node_fs_1.default
|
|
259
|
+
.readFileSync(acceptancePath, 'utf8')
|
|
260
|
+
.split(/\r?\n/)
|
|
261
|
+
.filter((line) => /^-\s+\[[ xX]\]/.test(line.trim()));
|
|
262
|
+
if (checklistLines.length === 0 || checklistLines.some((line) => /^-\s+\[\s\]/.test(line.trim()))) {
|
|
263
|
+
issues.push({
|
|
264
|
+
severity: 'error',
|
|
265
|
+
code: 'ACCEPTANCE_INCOMPLETE',
|
|
266
|
+
message: 'Done-level validation requires all acceptance checkboxes to be checked.',
|
|
267
|
+
path: relativePath
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function validateDoneLevelScaffoldContent(projectRoot, task, issues) {
|
|
272
|
+
const checks = [
|
|
273
|
+
{
|
|
274
|
+
fileName: 'TASK.md',
|
|
275
|
+
code: 'TASK_SCAFFOLD_PLACEHOLDER',
|
|
276
|
+
message: 'Done-level validation requires TASK.md Goal, Scope, and Out of Scope to replace scaffold placeholders.'
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
fileName: 'PLAN.md',
|
|
280
|
+
code: 'PLAN_SCAFFOLD_UNCHANGED',
|
|
281
|
+
message: 'Done-level validation requires PLAN.md to replace the default scaffold plan.'
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
fileName: 'CONTEXT.md',
|
|
285
|
+
code: 'CONTEXT_SCAFFOLD_UNCHANGED',
|
|
286
|
+
message: 'Done-level validation requires CONTEXT.md to contain task-specific context.'
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
fileName: 'FILES.md',
|
|
290
|
+
code: 'FILES_SCAFFOLD_UNCHANGED',
|
|
291
|
+
message: 'Done-level validation requires FILES.md to list touched files or explain that no files changed.'
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
fileName: 'ACCEPTANCE.md',
|
|
295
|
+
code: 'ACCEPTANCE_SCAFFOLD_UNCHANGED',
|
|
296
|
+
message: 'Done-level validation requires ACCEPTANCE.md to replace the default checklist items.'
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
fileName: 'TESTS.md',
|
|
300
|
+
code: 'TESTS_SCAFFOLD_UNCHANGED',
|
|
301
|
+
message: 'Done-level validation requires TESTS.md to replace the default npm test/npm run check scaffold.'
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
fileName: 'RISKS.md',
|
|
305
|
+
code: 'RISKS_SCAFFOLD_UNCHANGED',
|
|
306
|
+
message: 'Done-level validation requires RISKS.md to list risks or record why no material risks remain.'
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
fileName: 'DECISIONS.md',
|
|
310
|
+
code: 'DECISIONS_SCAFFOLD_UNCHANGED',
|
|
311
|
+
message: 'Done-level validation requires DECISIONS.md to replace the default scaffold note.'
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
fileName: 'EVIDENCE.md',
|
|
315
|
+
code: 'EVIDENCE_SCAFFOLD_UNCHANGED',
|
|
316
|
+
message: 'Done-level validation requires EVIDENCE.md to contain at least one evidence table row.'
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
for (const check of checks) {
|
|
320
|
+
const filePath = node_path_1.default.join(task.dir, check.fileName);
|
|
321
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
322
|
+
continue;
|
|
323
|
+
const content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
324
|
+
if ((0, task_capsule_1.isTaskCapsuleScaffoldContent)(task, check.fileName, content)) {
|
|
325
|
+
issues.push({
|
|
326
|
+
severity: 'error',
|
|
327
|
+
code: check.code,
|
|
328
|
+
message: check.message,
|
|
329
|
+
path: toPortablePath(node_path_1.default.relative(projectRoot, filePath))
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function validateEvidenceIndexHasRecords(projectRoot, task, issues) {
|
|
335
|
+
const indexPath = node_path_1.default.join(task.dir, 'evidence.jsonl');
|
|
336
|
+
if (!node_fs_1.default.existsSync(indexPath))
|
|
337
|
+
return;
|
|
338
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, indexPath));
|
|
339
|
+
if (!node_fs_1.default.readFileSync(indexPath, 'utf8').trim()) {
|
|
340
|
+
issues.push({
|
|
341
|
+
severity: 'error',
|
|
342
|
+
code: 'EVIDENCE_REQUIRED',
|
|
343
|
+
message: 'Done-level validation requires at least one evidence.jsonl record.',
|
|
344
|
+
path: relativePath
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function validateEvidenceMarkdownSingleTable(projectRoot, task, issues) {
|
|
349
|
+
const evidencePath = node_path_1.default.join(task.dir, 'EVIDENCE.md');
|
|
350
|
+
if (!node_fs_1.default.existsSync(evidencePath))
|
|
351
|
+
return;
|
|
352
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, evidencePath));
|
|
353
|
+
const lines = node_fs_1.default.readFileSync(evidencePath, 'utf8').split(/\r?\n/);
|
|
354
|
+
const tableHeaderCount = lines.filter((line, index) => line.trim() === '| Time | Kind | Summary | Result |' && lines[index + 1]?.trim() === '|---|---|---|---|').length;
|
|
355
|
+
if (tableHeaderCount > 1) {
|
|
356
|
+
issues.push({
|
|
357
|
+
severity: 'error',
|
|
358
|
+
code: 'EVIDENCE_TABLE_DUPLICATE_HEADER',
|
|
359
|
+
message: `Done-level validation requires EVIDENCE.md to contain exactly one evidence table header; found ${tableHeaderCount}.`,
|
|
360
|
+
path: relativePath
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function validateHandoffDone(projectRoot, task, issues) {
|
|
365
|
+
const handoffPath = node_path_1.default.join(task.dir, 'HANDOFF.md');
|
|
366
|
+
if (!node_fs_1.default.existsSync(handoffPath))
|
|
367
|
+
return;
|
|
368
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, handoffPath));
|
|
369
|
+
const lastCompleted = readSectionBody(handoffPath, '## Last Completed').trim();
|
|
370
|
+
const nextStep = readSectionBody(handoffPath, '## Next Recommended Step').trim();
|
|
371
|
+
if (isPlaceholderSection(lastCompleted) || isPlaceholderSection(nextStep)) {
|
|
372
|
+
issues.push({
|
|
373
|
+
severity: 'error',
|
|
374
|
+
code: 'HANDOFF_PLACEHOLDER',
|
|
375
|
+
message: 'Done-level validation requires non-placeholder handoff sections.',
|
|
376
|
+
path: relativePath
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
|
|
381
|
+
const taskBoardPath = node_path_1.default.join(projectRoot, 'docs', 'TASK_BOARD.md');
|
|
382
|
+
const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, taskBoardPath));
|
|
383
|
+
checkedFiles.push(relativePath);
|
|
384
|
+
if (!node_fs_1.default.existsSync(taskBoardPath)) {
|
|
385
|
+
issues.push({
|
|
386
|
+
severity: 'error',
|
|
387
|
+
code: 'TASK_BOARD_MISSING',
|
|
388
|
+
message: 'Done-level validation requires docs/TASK_BOARD.md to contain the completed task row.',
|
|
389
|
+
path: relativePath
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const rows = parseTaskBoardRows(node_fs_1.default.readFileSync(taskBoardPath, 'utf8')).filter((row) => row.id === task.id);
|
|
394
|
+
if (rows.length === 0) {
|
|
395
|
+
issues.push({
|
|
396
|
+
severity: 'error',
|
|
397
|
+
code: 'TASK_BOARD_ROW_MISSING',
|
|
398
|
+
message: `Done-level validation requires docs/TASK_BOARD.md to contain exactly one row for ${task.id}.`,
|
|
399
|
+
path: relativePath
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (rows.length > 1) {
|
|
404
|
+
issues.push({
|
|
405
|
+
severity: 'error',
|
|
406
|
+
code: 'TASK_BOARD_ROW_DUPLICATE',
|
|
407
|
+
message: `docs/TASK_BOARD.md contains ${rows.length} rows for ${task.id}; expected exactly one.`,
|
|
408
|
+
path: relativePath
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const row = rows[0];
|
|
413
|
+
const expectedCapsule = toPortablePath(node_path_1.default.relative(projectRoot, task.dir));
|
|
414
|
+
if (row.status !== 'Done') {
|
|
415
|
+
issues.push({
|
|
416
|
+
severity: 'error',
|
|
417
|
+
code: 'TASK_BOARD_STATUS_NOT_DONE',
|
|
418
|
+
message: `Done-level validation requires docs/TASK_BOARD.md status for ${task.id} to be Done.`,
|
|
419
|
+
path: relativePath
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (row.capsule !== expectedCapsule) {
|
|
423
|
+
issues.push({
|
|
424
|
+
severity: 'error',
|
|
425
|
+
code: 'TASK_BOARD_CAPSULE_MISMATCH',
|
|
426
|
+
message: `docs/TASK_BOARD.md capsule for ${task.id} is ${row.capsule || '(empty)'}, expected ${expectedCapsule}.`,
|
|
427
|
+
path: relativePath
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function parseTaskBoardRows(content) {
|
|
432
|
+
return content
|
|
433
|
+
.split(/\r?\n/)
|
|
434
|
+
.map((line) => line.trim())
|
|
435
|
+
.filter((line) => /^\|\s*T-\d{4}\s*\|/.test(line))
|
|
436
|
+
.map((line) => {
|
|
437
|
+
const cells = line
|
|
438
|
+
.slice(1, line.endsWith('|') ? -1 : undefined)
|
|
439
|
+
.split('|')
|
|
440
|
+
.map((cell) => cell.trim());
|
|
441
|
+
return {
|
|
442
|
+
id: cells[0] ?? '',
|
|
443
|
+
title: cells[1] ?? '',
|
|
444
|
+
status: cells[2] ?? '',
|
|
445
|
+
capsule: cells[3] ?? '',
|
|
446
|
+
notes: cells[4] ?? ''
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function readSectionBody(filePath, heading) {
|
|
451
|
+
const content = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
452
|
+
const start = content.indexOf(heading);
|
|
453
|
+
if (start < 0)
|
|
454
|
+
return '';
|
|
455
|
+
const afterHeading = content.slice(start + heading.length);
|
|
456
|
+
const nextHeading = afterHeading.search(/\n##\s+/);
|
|
457
|
+
return nextHeading >= 0 ? afterHeading.slice(0, nextHeading) : afterHeading;
|
|
458
|
+
}
|
|
459
|
+
function isPlaceholderSection(value) {
|
|
460
|
+
const normalized = value.trim();
|
|
461
|
+
return normalized.length === 0 || /^TBD\.?$/i.test(normalized);
|
|
462
|
+
}
|
|
463
|
+
function toPortablePath(value) {
|
|
464
|
+
return value.split(node_path_1.default.sep).join('/');
|
|
465
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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.detectHermesContext = detectHermesContext;
|
|
7
|
+
exports.buildHadaraContextContent = buildHadaraContextContent;
|
|
8
|
+
exports.createContextExportReport = createContextExportReport;
|
|
9
|
+
exports.exportHadaraContext = exportHadaraContext;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const fs_1 = require("../core/fs");
|
|
13
|
+
const HERMES_CONTEXT_FILES = ['AGENTS.md', '.hermes.md', 'HERMES.md', 'CLAUDE.md', '.cursorrules'];
|
|
14
|
+
const CONTEXT_OUTPUT_PATH = '.hadara/context/HADARA_CONTEXT.md';
|
|
15
|
+
function detectHermesContext(projectRoot) {
|
|
16
|
+
const found = [];
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const file of HERMES_CONTEXT_FILES) {
|
|
19
|
+
const filePath = node_path_1.default.join(projectRoot, file);
|
|
20
|
+
if (node_fs_1.default.existsSync(filePath))
|
|
21
|
+
found.push(file);
|
|
22
|
+
else
|
|
23
|
+
missing.push(file);
|
|
24
|
+
}
|
|
25
|
+
return { found, missing };
|
|
26
|
+
}
|
|
27
|
+
function buildHadaraContextContent(projectRoot) {
|
|
28
|
+
const sourceFiles = [
|
|
29
|
+
'docs/PROJECT_STATE.md',
|
|
30
|
+
'docs/AGENT_HANDOFF.md',
|
|
31
|
+
'docs/IMPLEMENTATION_SOP.md',
|
|
32
|
+
'docs/TASK_BOARD.md',
|
|
33
|
+
'docs/ROADMAP.md',
|
|
34
|
+
'docs/DEVELOPMENT_SLICES.md',
|
|
35
|
+
'docs/CLI_JSON_CONTRACT.md',
|
|
36
|
+
'docs/MCP_BRIDGE_CONTRACT.md',
|
|
37
|
+
'docs/MCP_EVIDENCE_ATTACH_CONTRACT.md',
|
|
38
|
+
'docs/ARCHITECTURE.md',
|
|
39
|
+
'docs/SECURITY_MODEL.md',
|
|
40
|
+
'docs/TEST_STRATEGY.md'
|
|
41
|
+
];
|
|
42
|
+
const sections = sourceFiles.map((relativePath) => {
|
|
43
|
+
const content = (0, fs_1.readTextIfExists)(node_path_1.default.join(projectRoot, relativePath)) ?? '_Missing._';
|
|
44
|
+
return `## ${relativePath}\n\n${content}`;
|
|
45
|
+
});
|
|
46
|
+
const output = `# HADARA_CONTEXT
|
|
47
|
+
|
|
48
|
+
This file is generated by HADARA for Hermes Agent and external agent-harness compatibility.
|
|
49
|
+
Do not treat this file as the source of truth; prefer HADARA CLI JSON, MCP read tools, and the referenced docs.
|
|
50
|
+
Follow docs/IMPLEMENTATION_SOP.md for implementation, validation, and session-end procedure.
|
|
51
|
+
|
|
52
|
+
Agents must:
|
|
53
|
+
1. Preserve the portable/project store boundary.
|
|
54
|
+
2. Work inside the active Task Capsule.
|
|
55
|
+
3. Attach evidence before marking work complete.
|
|
56
|
+
4. Update AGENT_HANDOFF.md before stopping.
|
|
57
|
+
5. Respect policy decisions for shell/file/git operations.
|
|
58
|
+
6. Treat AGENT_HANDOFF.md as compact current state and follow its Historical Index for older history.
|
|
59
|
+
7. Prefer stable HADARA read surfaces before scraping raw files: use hadara.project.state.read or hadara status --json for project state.
|
|
60
|
+
8. Use hadara.task.list and hadara.task.read, or hadara task list --json and hadara task show <task-id> --json, for task state.
|
|
61
|
+
9. Use hadara.handoff.read, hadara.policy.evaluate, and hadara.harness.validate, or their CLI JSON equivalents, for handoff, policy, and validation state.
|
|
62
|
+
10. Use hadara.context.export for in-memory MCP context export; only hadara hermes export-context writes .hadara/context/HADARA_CONTEXT.md.
|
|
63
|
+
11. Treat MCP default mode as read-only; do not assume MCP task mutation, file writes, shell execution, or release/package execution exists.
|
|
64
|
+
12. If MCP is unavailable, fall back to CLI JSON commands and then to repository documents.
|
|
65
|
+
13. Respect the single active agent/session model; do not assume queues, worker lanes, or multi-agent concurrent execution.
|
|
66
|
+
14. Treat policy.evaluate as policy evaluation only, not MCP execution authorization.
|
|
67
|
+
15. Treat MCP evidence attachment as disabled by default; it requires explicit server opt-in, per-call approval metadata, and private audit logging.
|
|
68
|
+
|
|
69
|
+
${sections.join('\n\n---\n\n')}
|
|
70
|
+
`;
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
function createContextExportReport(projectRoot, options = {}) {
|
|
74
|
+
const format = options.format ?? 'markdown';
|
|
75
|
+
const markdown = buildHadaraContextContent(projectRoot);
|
|
76
|
+
const content = format === 'json' ? JSON.stringify({ schemaVersion: 'hadara.context.content.v1', summaryOnly: options.summaryOnly === true, markdown }) : markdown;
|
|
77
|
+
const issues = options.summaryOnly === true
|
|
78
|
+
? [
|
|
79
|
+
{
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
code: 'SUMMARY_ONLY_NOT_IMPLEMENTED',
|
|
82
|
+
message: 'summaryOnly is accepted for forward compatibility but currently returns the full context.'
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
: [];
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: 'hadara.context.export.v1',
|
|
88
|
+
command: 'context.export',
|
|
89
|
+
ok: true,
|
|
90
|
+
format,
|
|
91
|
+
mode: 'memory',
|
|
92
|
+
content,
|
|
93
|
+
contextPath: null,
|
|
94
|
+
wouldWritePath: CONTEXT_OUTPUT_PATH,
|
|
95
|
+
issues
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function exportHadaraContext(projectRoot) {
|
|
99
|
+
const contextDir = node_path_1.default.join(projectRoot, '.hadara', 'context');
|
|
100
|
+
(0, fs_1.ensureDir)(contextDir);
|
|
101
|
+
const outputPath = node_path_1.default.join(projectRoot, CONTEXT_OUTPUT_PATH);
|
|
102
|
+
node_fs_1.default.writeFileSync(outputPath, buildHadaraContextContent(projectRoot), 'utf8');
|
|
103
|
+
return outputPath;
|
|
104
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./core/paths"), exports);
|
|
18
|
+
__exportStar(require("./core/redaction"), exports);
|
|
19
|
+
__exportStar(require("./core/workspace"), exports);
|
|
20
|
+
__exportStar(require("./cli/args"), exports);
|
|
21
|
+
__exportStar(require("./providers/provider-contract"), exports);
|
|
22
|
+
__exportStar(require("./providers/mock-provider"), exports);
|
|
23
|
+
__exportStar(require("./providers/provider-preparation"), exports);
|
|
24
|
+
__exportStar(require("./task/task-capsule"), exports);
|
|
25
|
+
__exportStar(require("./evidence/evidence"), exports);
|
|
26
|
+
__exportStar(require("./handoff/handoff"), exports);
|
|
27
|
+
__exportStar(require("./tools/fake-shell"), exports);
|
|
28
|
+
__exportStar(require("./agent/loop"), exports);
|
|
29
|
+
__exportStar(require("./agent/evidence"), exports);
|