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,341 @@
|
|
|
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.resolveTuiCacheRoot = resolveTuiCacheRoot;
|
|
7
|
+
exports.readTuiCache = readTuiCache;
|
|
8
|
+
exports.writeTuiCache = writeTuiCache;
|
|
9
|
+
exports.buildTaskIndex = buildTaskIndex;
|
|
10
|
+
exports.refreshTaskIndex = refreshTaskIndex;
|
|
11
|
+
exports.collectTuiCacheSourceSignals = collectTuiCacheSourceSignals;
|
|
12
|
+
exports.areTuiCacheSourceSignalsEqual = areTuiCacheSourceSignalsEqual;
|
|
13
|
+
exports.createTuiReadModelWithCache = createTuiReadModelWithCache;
|
|
14
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const workspace_1 = require("../core/workspace");
|
|
18
|
+
const evidence_list_1 = require("../services/evidence-list");
|
|
19
|
+
const read_model_1 = require("./read-model");
|
|
20
|
+
const CACHE_FILE = 'read-model-cache.json';
|
|
21
|
+
const TASK_CAPSULE_FILES = [
|
|
22
|
+
'TASK.md',
|
|
23
|
+
'PLAN.md',
|
|
24
|
+
'CONTEXT.md',
|
|
25
|
+
'ACCEPTANCE.md',
|
|
26
|
+
'FILES.md',
|
|
27
|
+
'TESTS.md',
|
|
28
|
+
'RISKS.md',
|
|
29
|
+
'DECISIONS.md',
|
|
30
|
+
'EVIDENCE.md',
|
|
31
|
+
'evidence.jsonl',
|
|
32
|
+
'HANDOFF.md'
|
|
33
|
+
];
|
|
34
|
+
function resolveTuiCacheRoot(projectRoot) {
|
|
35
|
+
return node_path_1.default.join(projectRoot, '.hadara', 'local', 'tui');
|
|
36
|
+
}
|
|
37
|
+
function readTuiCache(options) {
|
|
38
|
+
if (options.enabled === false)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const cachePath = tuiCacheFilePath(options);
|
|
42
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(cachePath, 'utf8'));
|
|
43
|
+
return isTuiCacheRecord(parsed, options.projectRoot) ? parsed : null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function writeTuiCache(options, record) {
|
|
50
|
+
if (options.enabled === false)
|
|
51
|
+
return;
|
|
52
|
+
const cacheRoot = normalizeTuiCacheRoot(options);
|
|
53
|
+
const cachePath = node_path_1.default.join(cacheRoot, CACHE_FILE);
|
|
54
|
+
assertTuiCachePath(options.projectRoot, cachePath, options.cacheRoot);
|
|
55
|
+
node_fs_1.default.mkdirSync(cacheRoot, { recursive: true });
|
|
56
|
+
node_fs_1.default.writeFileSync(cachePath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
function buildTaskIndex(projectRoot) {
|
|
59
|
+
const model = (0, read_model_1.createTuiReadModel)(projectRoot);
|
|
60
|
+
return buildTaskIndexFromSummaries(projectRoot, model.tasks.tasks);
|
|
61
|
+
}
|
|
62
|
+
function refreshTaskIndex(projectRoot, previous) {
|
|
63
|
+
const previousById = new Map(previous.map((entry) => [entry.id, entry]));
|
|
64
|
+
return buildTaskIndexFromSummaries(projectRoot, previous.map((entry) => ({
|
|
65
|
+
id: entry.id,
|
|
66
|
+
title: entry.title,
|
|
67
|
+
status: entry.status,
|
|
68
|
+
slug: node_path_1.default.basename(entry.capsule).replace(/^T-\d{4}-/, ''),
|
|
69
|
+
capsule: entry.capsule
|
|
70
|
+
})), previousById);
|
|
71
|
+
}
|
|
72
|
+
function collectTuiCacheSourceSignals(projectRoot, selectedTask, previous) {
|
|
73
|
+
return {
|
|
74
|
+
taskBoard: fileSignal(projectRoot, 'docs/TASK_BOARD.md', true, previous?.taskBoard),
|
|
75
|
+
tasksDir: directorySignal(projectRoot, 'tasks'),
|
|
76
|
+
handoff: fileSignal(projectRoot, 'docs/AGENT_HANDOFF.md', true, previous?.handoff),
|
|
77
|
+
activeRun: fileSignal(projectRoot, '.hadara/local/state/active-run.json', true, previous?.activeRun),
|
|
78
|
+
selectedTask: selectedTask ? fileSignal(projectRoot, node_path_1.default.join(selectedTask.capsule, 'TASK.md'), true, previous?.selectedTask) : undefined,
|
|
79
|
+
selectedEvidence: selectedTask ? fileSignal(projectRoot, node_path_1.default.join(selectedTask.capsule, 'evidence.jsonl'), true, previous?.selectedEvidence) : undefined
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function areTuiCacheSourceSignalsEqual(left, right) {
|
|
83
|
+
if (!left)
|
|
84
|
+
return false;
|
|
85
|
+
return (fileSignalsEqual(left.taskBoard, right.taskBoard) &&
|
|
86
|
+
directorySignalsEqual(left.tasksDir, right.tasksDir) &&
|
|
87
|
+
fileSignalsEqual(left.handoff, right.handoff) &&
|
|
88
|
+
fileSignalsEqual(left.activeRun, right.activeRun) &&
|
|
89
|
+
fileSignalsEqual(left.selectedTask, right.selectedTask) &&
|
|
90
|
+
fileSignalsEqual(left.selectedEvidence, right.selectedEvidence));
|
|
91
|
+
}
|
|
92
|
+
function sourceSignalsForCachedSelection(projectRoot, cached) {
|
|
93
|
+
return collectTuiCacheSourceSignals(projectRoot, cached.model.selectedTask?.summary ?? null, cached.sourceSignals);
|
|
94
|
+
}
|
|
95
|
+
function validateCachedRecord(projectRoot, cached) {
|
|
96
|
+
if (!cached)
|
|
97
|
+
return { valid: false, taskIndex: [] };
|
|
98
|
+
const sourceSignals = sourceSignalsForCachedSelection(projectRoot, cached);
|
|
99
|
+
if (!areTuiCacheSourceSignalsEqual(cached.sourceSignals, sourceSignals))
|
|
100
|
+
return { valid: false, taskIndex: [] };
|
|
101
|
+
const previousById = new Map(cached.taskIndex.map((entry) => [entry.id, entry]));
|
|
102
|
+
const taskIndex = buildTaskIndexFromSummaries(projectRoot, cached.model.tasks.tasks, previousById);
|
|
103
|
+
return { valid: taskIndexesEqual(cached.taskIndex, taskIndex), taskIndex };
|
|
104
|
+
}
|
|
105
|
+
function disablePrivateEvidenceCache(projectRoot, cachePath, refresh, options) {
|
|
106
|
+
return {
|
|
107
|
+
model: (0, read_model_1.createTuiReadModel)(projectRoot, options),
|
|
108
|
+
cache: {
|
|
109
|
+
enabled: false,
|
|
110
|
+
refresh,
|
|
111
|
+
hit: false,
|
|
112
|
+
path: (0, workspace_1.toProjectRelativePath)(projectRoot, cachePath),
|
|
113
|
+
issues: [
|
|
114
|
+
{
|
|
115
|
+
severity: 'warning',
|
|
116
|
+
code: 'TUI_PRIVATE_EVIDENCE_CACHE_DISABLED',
|
|
117
|
+
message: 'TUI cache is disabled when includePrivateEvidence is true.'
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function createTuiReadModelWithCache(projectRoot, options = {}) {
|
|
124
|
+
const refresh = options.cache?.refresh ?? 'fast';
|
|
125
|
+
const cacheRoot = options.cache?.root ?? resolveTuiCacheRoot(projectRoot);
|
|
126
|
+
const cachePath = node_path_1.default.join(cacheRoot, CACHE_FILE);
|
|
127
|
+
const cacheOptions = { projectRoot, cacheRoot, enabled: options.cache?.enabled };
|
|
128
|
+
const issues = [];
|
|
129
|
+
if (options.includePrivateEvidence === true) {
|
|
130
|
+
return disablePrivateEvidenceCache(projectRoot, cachePath, refresh, options);
|
|
131
|
+
}
|
|
132
|
+
if (options.cache?.enabled === false || refresh === 'none') {
|
|
133
|
+
return {
|
|
134
|
+
model: (0, read_model_1.createTuiReadModel)(projectRoot, options),
|
|
135
|
+
cache: { enabled: options.cache?.enabled !== false, refresh, hit: false, path: (0, workspace_1.toProjectRelativePath)(projectRoot, cachePath), issues }
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const cached = readTuiCache(cacheOptions);
|
|
139
|
+
const validation = validateCachedRecord(projectRoot, cached);
|
|
140
|
+
const valid = validation.valid;
|
|
141
|
+
if (refresh === 'detail' && cached && valid && options.selectedTaskId) {
|
|
142
|
+
const selectedSummary = cached.model.tasks.tasks.find((task) => task.id === options.selectedTaskId) ?? null;
|
|
143
|
+
if (selectedSummary) {
|
|
144
|
+
const model = {
|
|
145
|
+
...cached.model,
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
selectedTaskId: options.selectedTaskId,
|
|
148
|
+
selectedTask: createSelectedTask(projectRoot, selectedSummary, options)
|
|
149
|
+
};
|
|
150
|
+
writeTuiCache(cacheOptions, createCacheRecord(projectRoot, model, validation.taskIndex));
|
|
151
|
+
return { model, cache: { enabled: true, refresh, hit: true, path: (0, workspace_1.toProjectRelativePath)(projectRoot, cachePath), issues } };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (refresh === 'fast' && cached && valid) {
|
|
155
|
+
return { model: cached.model, cache: { enabled: true, refresh, hit: true, path: (0, workspace_1.toProjectRelativePath)(projectRoot, cachePath), issues } };
|
|
156
|
+
}
|
|
157
|
+
const model = (0, read_model_1.createTuiReadModel)(projectRoot, options);
|
|
158
|
+
try {
|
|
159
|
+
writeTuiCache(cacheOptions, createCacheRecord(projectRoot, model, buildTaskIndexFromSummaries(projectRoot, model.tasks.tasks)));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
issues.push({
|
|
163
|
+
severity: 'warning',
|
|
164
|
+
code: 'TUI_CACHE_WRITE_FAILED',
|
|
165
|
+
message: `TUI cache could not be written: ${error instanceof Error ? error.message : String(error)}`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { model, cache: { enabled: true, refresh, hit: false, path: (0, workspace_1.toProjectRelativePath)(projectRoot, cachePath), issues } };
|
|
169
|
+
}
|
|
170
|
+
function createSelectedTask(projectRoot, summary, options) {
|
|
171
|
+
const detail = createTaskReadReportFromSummary(projectRoot, summary, options);
|
|
172
|
+
const evidenceRecords = detail.evidenceIndex ?? [];
|
|
173
|
+
const limit = Math.max(0, Math.floor(options.evidenceLimit ?? 20));
|
|
174
|
+
return {
|
|
175
|
+
summary,
|
|
176
|
+
detail,
|
|
177
|
+
evidence: {
|
|
178
|
+
schemaVersion: 'hadara.evidence.list.v1',
|
|
179
|
+
command: 'evidence.list',
|
|
180
|
+
ok: detail.ok,
|
|
181
|
+
taskId: summary.id,
|
|
182
|
+
count: evidenceRecords.slice(0, limit).length,
|
|
183
|
+
records: evidenceRecords.slice(0, limit),
|
|
184
|
+
issues: detail.issues
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function createTaskReadReportFromSummary(projectRoot, summary, options) {
|
|
189
|
+
const taskDir = node_path_1.default.join(projectRoot, summary.capsule);
|
|
190
|
+
(0, workspace_1.assertInsideProject)(projectRoot, taskDir, summary.capsule);
|
|
191
|
+
const files = Object.fromEntries(TASK_CAPSULE_FILES.map((fileName) => {
|
|
192
|
+
const filePath = node_path_1.default.join(taskDir, fileName);
|
|
193
|
+
return [fileName, node_fs_1.default.existsSync(filePath) ? node_fs_1.default.readFileSync(filePath, 'utf8') : ''];
|
|
194
|
+
}));
|
|
195
|
+
const parsed = (0, evidence_list_1.parseEvidenceIndexFile)(node_path_1.default.join(taskDir, 'evidence.jsonl'), summary.id);
|
|
196
|
+
const includePrivate = options.includePrivateEvidence === true;
|
|
197
|
+
const evidenceIndex = parsed.records.filter((record) => includePrivate || record.visibility !== 'private');
|
|
198
|
+
files['evidence.jsonl'] = evidenceIndex.length ? `${evidenceIndex.map((record) => JSON.stringify(record)).join('\n')}\n` : '';
|
|
199
|
+
return {
|
|
200
|
+
schemaVersion: 'hadara.task.read.v1',
|
|
201
|
+
command: 'task.read',
|
|
202
|
+
ok: !parsed.issues.some((issue) => issue.severity === 'error'),
|
|
203
|
+
task: summary,
|
|
204
|
+
files,
|
|
205
|
+
evidenceIndex,
|
|
206
|
+
issues: parsed.issues
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function createCacheRecord(projectRoot, model, taskIndex) {
|
|
210
|
+
return {
|
|
211
|
+
schemaVersion: 'hadara.tui.cache.v1',
|
|
212
|
+
projectRoot: '.',
|
|
213
|
+
generatedAt: new Date().toISOString(),
|
|
214
|
+
sourceSignals: collectTuiCacheSourceSignals(projectRoot, model.selectedTask?.summary ?? null),
|
|
215
|
+
taskIndex,
|
|
216
|
+
model
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function buildTaskIndexFromSummaries(projectRoot, tasks, previousById = new Map()) {
|
|
220
|
+
return tasks.map((task) => {
|
|
221
|
+
const taskPath = node_path_1.default.join(projectRoot, task.capsule, 'TASK.md');
|
|
222
|
+
const previous = previousById.get(task.id);
|
|
223
|
+
if (!node_fs_1.default.existsSync(taskPath)) {
|
|
224
|
+
return {
|
|
225
|
+
id: task.id,
|
|
226
|
+
title: task.title,
|
|
227
|
+
status: task.status,
|
|
228
|
+
capsule: task.capsule,
|
|
229
|
+
mtimeMs: 0,
|
|
230
|
+
size: 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const stat = node_fs_1.default.statSync(taskPath);
|
|
234
|
+
const hash = previous && previous.mtimeMs === stat.mtimeMs && previous.size === stat.size && previous.hash
|
|
235
|
+
? previous.hash
|
|
236
|
+
: node_crypto_1.default.createHash('sha256').update(node_fs_1.default.readFileSync(taskPath)).digest('hex');
|
|
237
|
+
return {
|
|
238
|
+
id: task.id,
|
|
239
|
+
title: task.title,
|
|
240
|
+
status: task.status,
|
|
241
|
+
capsule: task.capsule,
|
|
242
|
+
mtimeMs: stat.mtimeMs,
|
|
243
|
+
size: stat.size,
|
|
244
|
+
hash
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function taskIndexesEqual(left, right) {
|
|
249
|
+
if (left.length !== right.length)
|
|
250
|
+
return false;
|
|
251
|
+
return left.every((entry, index) => {
|
|
252
|
+
const other = right[index];
|
|
253
|
+
return Boolean(other &&
|
|
254
|
+
entry.id === other.id &&
|
|
255
|
+
entry.capsule === other.capsule &&
|
|
256
|
+
entry.mtimeMs === other.mtimeMs &&
|
|
257
|
+
entry.size === other.size &&
|
|
258
|
+
entry.hash === other.hash);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function tuiCacheFilePath(options) {
|
|
262
|
+
const cacheRoot = normalizeTuiCacheRoot(options);
|
|
263
|
+
const cachePath = node_path_1.default.join(cacheRoot, CACHE_FILE);
|
|
264
|
+
assertTuiCachePath(options.projectRoot, cachePath, options.cacheRoot);
|
|
265
|
+
return cachePath;
|
|
266
|
+
}
|
|
267
|
+
function normalizeTuiCacheRoot(options) {
|
|
268
|
+
return options.cacheRoot ?? resolveTuiCacheRoot(options.projectRoot);
|
|
269
|
+
}
|
|
270
|
+
function assertTuiCachePath(projectRoot, candidatePath, explicitRoot) {
|
|
271
|
+
const root = explicitRoot ?? resolveTuiCacheRoot(projectRoot);
|
|
272
|
+
(0, workspace_1.assertInsideProject)(projectRoot, root);
|
|
273
|
+
if (node_path_1.default.relative(resolveTuiCacheRoot(projectRoot), root).startsWith('..')) {
|
|
274
|
+
throw new Error('TUI cache root must stay under .hadara/local/tui.');
|
|
275
|
+
}
|
|
276
|
+
if (node_path_1.default.relative(root, candidatePath).startsWith('..')) {
|
|
277
|
+
throw new Error('TUI cache path must stay under the configured cache root.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function isTuiCacheRecord(value, projectRoot) {
|
|
281
|
+
if (typeof value !== 'object' || value === null)
|
|
282
|
+
return false;
|
|
283
|
+
const record = value;
|
|
284
|
+
return (record.schemaVersion === 'hadara.tui.cache.v1' &&
|
|
285
|
+
record.projectRoot === '.' &&
|
|
286
|
+
typeof record.generatedAt === 'string' &&
|
|
287
|
+
typeof record.sourceSignals === 'object' &&
|
|
288
|
+
record.sourceSignals !== null &&
|
|
289
|
+
Array.isArray(record.taskIndex) &&
|
|
290
|
+
typeof record.model === 'object' &&
|
|
291
|
+
record.model !== null &&
|
|
292
|
+
record.model.schemaVersion === 'hadara.tui.read_model.internal.v1');
|
|
293
|
+
}
|
|
294
|
+
function fileSignal(projectRoot, relativePath, includeHash, previous) {
|
|
295
|
+
const filePath = node_path_1.default.join(projectRoot, relativePath);
|
|
296
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
297
|
+
return undefined;
|
|
298
|
+
const stat = node_fs_1.default.statSync(filePath);
|
|
299
|
+
if (!stat.isFile())
|
|
300
|
+
return undefined;
|
|
301
|
+
const signal = {
|
|
302
|
+
mtimeMs: stat.mtimeMs,
|
|
303
|
+
size: stat.size
|
|
304
|
+
};
|
|
305
|
+
if (includeHash) {
|
|
306
|
+
signal.hash =
|
|
307
|
+
previous && previous.mtimeMs === signal.mtimeMs && previous.size === signal.size && previous.hash
|
|
308
|
+
? previous.hash
|
|
309
|
+
: node_crypto_1.default.createHash('sha256').update(node_fs_1.default.readFileSync(filePath)).digest('hex');
|
|
310
|
+
}
|
|
311
|
+
return signal;
|
|
312
|
+
}
|
|
313
|
+
function directorySignal(projectRoot, relativePath) {
|
|
314
|
+
const dirPath = node_path_1.default.join(projectRoot, relativePath);
|
|
315
|
+
if (!node_fs_1.default.existsSync(dirPath))
|
|
316
|
+
return undefined;
|
|
317
|
+
const stat = node_fs_1.default.statSync(dirPath);
|
|
318
|
+
if (!stat.isDirectory())
|
|
319
|
+
return undefined;
|
|
320
|
+
return {
|
|
321
|
+
entries: node_fs_1.default
|
|
322
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
323
|
+
.filter((entry) => entry.isDirectory() && /^T-\d{4}-/.test(entry.name))
|
|
324
|
+
.map((entry) => entry.name)
|
|
325
|
+
.sort(),
|
|
326
|
+
mtimeMs: stat.mtimeMs
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function fileSignalsEqual(left, right) {
|
|
330
|
+
if (!left || !right)
|
|
331
|
+
return left === right;
|
|
332
|
+
return left.mtimeMs === right.mtimeMs && left.size === right.size && left.hash === right.hash;
|
|
333
|
+
}
|
|
334
|
+
function directorySignalsEqual(left, right) {
|
|
335
|
+
if (!left || !right)
|
|
336
|
+
return left === right;
|
|
337
|
+
return left.mtimeMs === right.mtimeMs && stringArraysEqual(left.entries, right.entries);
|
|
338
|
+
}
|
|
339
|
+
function stringArraysEqual(left, right) {
|
|
340
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
341
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TUI_DOCUMENT_TABS = exports.TUI_PANEL_LABELS = exports.TUI_PANEL_IDS = void 0;
|
|
4
|
+
exports.resolveTuiPanelId = resolveTuiPanelId;
|
|
5
|
+
exports.resolveTuiDocumentTab = resolveTuiDocumentTab;
|
|
6
|
+
exports.tuiDetailDocumentRowsForAvailableRows = tuiDetailDocumentRowsForAvailableRows;
|
|
7
|
+
exports.tuiDetailPanelRowsForAvailableRows = tuiDetailPanelRowsForAvailableRows;
|
|
8
|
+
exports.tuiTaskVisibleRowsForAvailableRows = tuiTaskVisibleRowsForAvailableRows;
|
|
9
|
+
exports.TUI_PANEL_IDS = ['overview', 'tasks', 'detail', 'help'];
|
|
10
|
+
const DETAIL_DOCUMENT_MAX_ROWS = 18;
|
|
11
|
+
exports.TUI_PANEL_LABELS = {
|
|
12
|
+
overview: 'Overview',
|
|
13
|
+
tasks: 'Tasks',
|
|
14
|
+
detail: 'Detail',
|
|
15
|
+
help: 'Help'
|
|
16
|
+
};
|
|
17
|
+
exports.TUI_DOCUMENT_TABS = [
|
|
18
|
+
{ label: 'Task', file: 'TASK.md', key: 't', shortLabel: 'TASK' },
|
|
19
|
+
{ label: 'Plan', file: 'PLAN.md', key: 'p', shortLabel: 'PLAN' },
|
|
20
|
+
{ label: 'Decisions', file: 'DECISIONS.md', key: 'd', shortLabel: 'DEC' },
|
|
21
|
+
{ label: 'Acceptance', file: 'ACCEPTANCE.md', key: 'a', shortLabel: 'ACC' },
|
|
22
|
+
{ label: 'Evidence', file: 'EVIDENCE.md', key: 'e', shortLabel: 'EVD' },
|
|
23
|
+
{ label: 'Handoff', file: 'HANDOFF.md', key: 'h', shortLabel: 'HAND' },
|
|
24
|
+
{ label: 'Files', file: 'FILES.md', key: 'f', shortLabel: 'FILE' },
|
|
25
|
+
{ label: 'Risks', file: 'RISKS.md', key: 'k', shortLabel: 'RISK' },
|
|
26
|
+
{ label: 'Tests', file: 'TESTS.md', key: 's', shortLabel: 'TEST' }
|
|
27
|
+
];
|
|
28
|
+
function resolveTuiPanelId(value, fallback = 'overview') {
|
|
29
|
+
const normalized = String(value ?? '').toLowerCase();
|
|
30
|
+
return exports.TUI_PANEL_IDS.find((panel) => panel === normalized) ?? fallback;
|
|
31
|
+
}
|
|
32
|
+
function resolveTuiDocumentTab(file) {
|
|
33
|
+
const normalized = String(file ?? '').toLowerCase();
|
|
34
|
+
return (exports.TUI_DOCUMENT_TABS.find((tab) => [tab.label, tab.file, tab.key, tab.shortLabel].some((candidate) => candidate.toLowerCase() === normalized)) ?? exports.TUI_DOCUMENT_TABS[0]);
|
|
35
|
+
}
|
|
36
|
+
function tuiDetailDocumentRowsForAvailableRows(availableRows) {
|
|
37
|
+
return Math.max(1, Math.min(DETAIL_DOCUMENT_MAX_ROWS, Math.floor(availableRows) - 9));
|
|
38
|
+
}
|
|
39
|
+
function tuiDetailPanelRowsForAvailableRows(availableRows) {
|
|
40
|
+
return tuiDetailDocumentRowsForAvailableRows(availableRows) + 9;
|
|
41
|
+
}
|
|
42
|
+
function tuiTaskVisibleRowsForAvailableRows(availableRows) {
|
|
43
|
+
return Math.max(1, tuiDetailPanelRowsForAvailableRows(availableRows) - 3);
|
|
44
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripAnsi = stripAnsi;
|
|
4
|
+
exports.visibleWidth = visibleWidth;
|
|
5
|
+
exports.repeat = repeat;
|
|
6
|
+
exports.fit = fit;
|
|
7
|
+
exports.fitAnsi = fitAnsi;
|
|
8
|
+
exports.trimFit = trimFit;
|
|
9
|
+
exports.trimFitAnsi = trimFitAnsi;
|
|
10
|
+
exports.pad = pad;
|
|
11
|
+
exports.padAnsi = padAnsi;
|
|
12
|
+
exports.divider = divider;
|
|
13
|
+
exports.badge = badge;
|
|
14
|
+
exports.statusRole = statusRole;
|
|
15
|
+
exports.card = card;
|
|
16
|
+
exports.columns = columns;
|
|
17
|
+
const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
18
|
+
function stripAnsi(text) {
|
|
19
|
+
return String(text).replace(ANSI_PATTERN, '');
|
|
20
|
+
}
|
|
21
|
+
function visibleWidth(text) {
|
|
22
|
+
let width = 0;
|
|
23
|
+
for (const char of Array.from(stripAnsi(text))) {
|
|
24
|
+
width += (char.codePointAt(0) ?? 0) > 0x2e80 ? 2 : 1;
|
|
25
|
+
}
|
|
26
|
+
return width;
|
|
27
|
+
}
|
|
28
|
+
function repeat(char, width) {
|
|
29
|
+
return char.repeat(Math.max(0, width));
|
|
30
|
+
}
|
|
31
|
+
function fit(input, width) {
|
|
32
|
+
const target = Math.max(0, width);
|
|
33
|
+
const text = stripAnsi(input);
|
|
34
|
+
if (visibleWidth(text) <= target)
|
|
35
|
+
return `${text}${repeat(' ', target - visibleWidth(text))}`;
|
|
36
|
+
if (target <= 1)
|
|
37
|
+
return text.slice(0, target);
|
|
38
|
+
let out = '';
|
|
39
|
+
let used = 0;
|
|
40
|
+
for (const char of Array.from(text)) {
|
|
41
|
+
const charWidth = (char.codePointAt(0) ?? 0) > 0x2e80 ? 2 : 1;
|
|
42
|
+
if (used + charWidth > target - 1)
|
|
43
|
+
break;
|
|
44
|
+
out += char;
|
|
45
|
+
used += charWidth;
|
|
46
|
+
}
|
|
47
|
+
return `${out}…${repeat(' ', Math.max(0, target - used - 1))}`;
|
|
48
|
+
}
|
|
49
|
+
function fitAnsi(input, width) {
|
|
50
|
+
const target = Math.max(0, width);
|
|
51
|
+
const text = String(input);
|
|
52
|
+
const currentWidth = visibleWidth(text);
|
|
53
|
+
if (currentWidth <= target)
|
|
54
|
+
return `${text}${repeat(' ', target - currentWidth)}`;
|
|
55
|
+
return truncateAnsi(text, target);
|
|
56
|
+
}
|
|
57
|
+
function trimFit(input, width) {
|
|
58
|
+
return fit(input, width).trimEnd();
|
|
59
|
+
}
|
|
60
|
+
function trimFitAnsi(input, width) {
|
|
61
|
+
return fitAnsi(input, width).trimEnd();
|
|
62
|
+
}
|
|
63
|
+
function pad(input, width) {
|
|
64
|
+
const text = String(input);
|
|
65
|
+
const length = visibleWidth(text);
|
|
66
|
+
if (length > width)
|
|
67
|
+
return fit(text, width);
|
|
68
|
+
return `${text}${repeat(' ', width - length)}`;
|
|
69
|
+
}
|
|
70
|
+
function padAnsi(input, width) {
|
|
71
|
+
const text = String(input);
|
|
72
|
+
const length = visibleWidth(text);
|
|
73
|
+
if (length > width)
|
|
74
|
+
return fitAnsi(text, width);
|
|
75
|
+
return `${text}${repeat(' ', width - length)}`;
|
|
76
|
+
}
|
|
77
|
+
function divider(width) {
|
|
78
|
+
return repeat('─', Math.max(0, width));
|
|
79
|
+
}
|
|
80
|
+
function badge(text, kind = 'LIVE') {
|
|
81
|
+
return `[${String(text || kind).toUpperCase()}]`;
|
|
82
|
+
}
|
|
83
|
+
function statusRole(value) {
|
|
84
|
+
const normalized = String(value ?? '').toLowerCase();
|
|
85
|
+
if (['ok', 'done', 'passed', 'true', 'read', 'preview'].includes(normalized))
|
|
86
|
+
return 'pass';
|
|
87
|
+
if (['warning', 'partial', 'draft', 'medium'].includes(normalized))
|
|
88
|
+
return 'warn';
|
|
89
|
+
if (['error', 'failed', 'high', 'disabled', 'blocked'].includes(normalized))
|
|
90
|
+
return 'fail';
|
|
91
|
+
return 'teal';
|
|
92
|
+
}
|
|
93
|
+
function card(title, lines, width) {
|
|
94
|
+
const inner = Math.max(8, width - 4);
|
|
95
|
+
const head = ` ${title} `;
|
|
96
|
+
return [
|
|
97
|
+
`╭─${fit(head, Math.min(visibleWidth(head), inner))}${repeat('─', Math.max(0, inner - visibleWidth(head)))}─╮`,
|
|
98
|
+
...lines.map((line) => `│ ${pad(line, inner)} │`),
|
|
99
|
+
`╰${repeat('─', inner + 2)}╯`
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
function columns(left, right, width, ratio = 0.52) {
|
|
103
|
+
const gap = 2;
|
|
104
|
+
const leftWidth = Math.max(20, Math.floor((width - gap) * ratio));
|
|
105
|
+
const rightWidth = Math.max(20, width - gap - leftWidth);
|
|
106
|
+
const rows = Math.max(left.length, right.length);
|
|
107
|
+
const output = [];
|
|
108
|
+
for (let index = 0; index < rows; index += 1) {
|
|
109
|
+
output.push(`${pad(left[index] ?? '', leftWidth)}${repeat(' ', gap)}${pad(right[index] ?? '', rightWidth)}`);
|
|
110
|
+
}
|
|
111
|
+
return output;
|
|
112
|
+
}
|
|
113
|
+
function truncateAnsi(text, width) {
|
|
114
|
+
if (width <= 0)
|
|
115
|
+
return '';
|
|
116
|
+
if (width <= 1)
|
|
117
|
+
return stripAnsi(text).slice(0, width);
|
|
118
|
+
let output = '';
|
|
119
|
+
let used = 0;
|
|
120
|
+
let index = 0;
|
|
121
|
+
let sawAnsi = false;
|
|
122
|
+
while (index < text.length) {
|
|
123
|
+
const ansi = text.slice(index).match(/^\x1b\[[0-9;?]*[ -/]*[@-~]/);
|
|
124
|
+
if (ansi) {
|
|
125
|
+
output += ansi[0];
|
|
126
|
+
sawAnsi = true;
|
|
127
|
+
index += ansi[0].length;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const char = Array.from(text.slice(index))[0] ?? '';
|
|
131
|
+
const charWidth = (char.codePointAt(0) ?? 0) > 0x2e80 ? 2 : 1;
|
|
132
|
+
if (used + charWidth > width - 1)
|
|
133
|
+
break;
|
|
134
|
+
output += char;
|
|
135
|
+
used += charWidth;
|
|
136
|
+
index += char.length;
|
|
137
|
+
}
|
|
138
|
+
const suffix = `${output}…${repeat(' ', Math.max(0, width - used - 1))}`;
|
|
139
|
+
return sawAnsi ? `${suffix}\x1b[0m` : suffix;
|
|
140
|
+
}
|