hadara 0.1.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/agent/evidence.js +50 -0
  4. package/dist/agent/loop.js +124 -0
  5. package/dist/cli/args.js +70 -0
  6. package/dist/cli/dashboard.js +185 -0
  7. package/dist/cli/debt.js +41 -0
  8. package/dist/cli/doctor.js +68 -0
  9. package/dist/cli/errors.js +58 -0
  10. package/dist/cli/evidence-json.js +75 -0
  11. package/dist/cli/evidence.js +80 -0
  12. package/dist/cli/handoff.js +16 -0
  13. package/dist/cli/harness.js +57 -0
  14. package/dist/cli/hermes-json.js +31 -0
  15. package/dist/cli/hermes.js +28 -0
  16. package/dist/cli/init.js +142 -0
  17. package/dist/cli/install.js +34 -0
  18. package/dist/cli/main.js +216 -0
  19. package/dist/cli/mcp.js +15 -0
  20. package/dist/cli/package-smoke.js +37 -0
  21. package/dist/cli/policy-json.js +22 -0
  22. package/dist/cli/policy.js +43 -0
  23. package/dist/cli/release-artifact.js +47 -0
  24. package/dist/cli/release-dry-run.js +24 -0
  25. package/dist/cli/release-gate.js +28 -0
  26. package/dist/cli/release-publish.js +41 -0
  27. package/dist/cli/run-scaffold.js +68 -0
  28. package/dist/cli/run-state.js +41 -0
  29. package/dist/cli/run.js +191 -0
  30. package/dist/cli/smoke.js +58 -0
  31. package/dist/cli/status-json.js +6 -0
  32. package/dist/cli/status.js +26 -0
  33. package/dist/cli/task-json.js +8 -0
  34. package/dist/cli/task.js +64 -0
  35. package/dist/cli/tools.js +25 -0
  36. package/dist/cli/tui.js +72 -0
  37. package/dist/cli/write-preflight.js +27 -0
  38. package/dist/core/audit.js +41 -0
  39. package/dist/core/events.js +63 -0
  40. package/dist/core/fs.js +44 -0
  41. package/dist/core/paths.js +59 -0
  42. package/dist/core/redaction.js +178 -0
  43. package/dist/core/schema.js +253 -0
  44. package/dist/core/workspace.js +47 -0
  45. package/dist/evidence/evidence.js +170 -0
  46. package/dist/evidence/private-manifest.js +101 -0
  47. package/dist/handoff/handoff.js +49 -0
  48. package/dist/harness/replay.js +200 -0
  49. package/dist/harness/validate.js +465 -0
  50. package/dist/hermes/context-export.js +104 -0
  51. package/dist/index.js +29 -0
  52. package/dist/mcp/server.js +104 -0
  53. package/dist/mcp/tool-dispatch.js +159 -0
  54. package/dist/mcp/tool-registry.js +150 -0
  55. package/dist/mcp/tool-schemas.js +18 -0
  56. package/dist/policy/command-risk.js +39 -0
  57. package/dist/policy/permission-matrix.js +42 -0
  58. package/dist/policy/policy.js +20 -0
  59. package/dist/policy/preflight.js +47 -0
  60. package/dist/policy/presets.js +24 -0
  61. package/dist/policy/tokenizer.js +53 -0
  62. package/dist/providers/fallback-executor.js +46 -0
  63. package/dist/providers/mock-provider.js +49 -0
  64. package/dist/providers/provider-contract.js +2 -0
  65. package/dist/providers/provider-preparation.js +220 -0
  66. package/dist/providers/scripted-provider.js +69 -0
  67. package/dist/schemas/active-run-projection.schema.json +73 -0
  68. package/dist/schemas/active-run-resume.schema.json +68 -0
  69. package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
  70. package/dist/schemas/context-export.schema.json +35 -0
  71. package/dist/schemas/event.schema.json +17 -0
  72. package/dist/schemas/evidence-list.schema.json +49 -0
  73. package/dist/schemas/feature-smoke.schema.json +67 -0
  74. package/dist/schemas/install-plan.schema.json +93 -0
  75. package/dist/schemas/package-smoke.schema.json +130 -0
  76. package/dist/schemas/private-evidence.schema.json +48 -0
  77. package/dist/schemas/provider-call.schema.json +42 -0
  78. package/dist/schemas/provider-config.schema.json +43 -0
  79. package/dist/schemas/release-artifact-manifest.schema.json +55 -0
  80. package/dist/schemas/release-artifact.schema.json +140 -0
  81. package/dist/schemas/release-dry-run.schema.json +141 -0
  82. package/dist/schemas/release-gate.schema.json +42 -0
  83. package/dist/schemas/release-publish.schema.json +114 -0
  84. package/dist/schemas/schema-index.json +145 -0
  85. package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
  86. package/dist/schemas/tools-list.schema.json +78 -0
  87. package/dist/schemas/write-preflight.schema.json +47 -0
  88. package/dist/services/active-run-state.js +215 -0
  89. package/dist/services/capability-registry.js +540 -0
  90. package/dist/services/clean-checkout-smoke.js +393 -0
  91. package/dist/services/evidence-list.js +136 -0
  92. package/dist/services/feature-smoke.js +155 -0
  93. package/dist/services/harness-service.js +7 -0
  94. package/dist/services/install-plan.js +233 -0
  95. package/dist/services/operational-debt.js +767 -0
  96. package/dist/services/operations-status-service.js +195 -0
  97. package/dist/services/package-smoke.js +676 -0
  98. package/dist/services/policy-service.js +25 -0
  99. package/dist/services/project-read-model.js +101 -0
  100. package/dist/services/release-artifact-evidence.js +77 -0
  101. package/dist/services/release-artifact.js +351 -0
  102. package/dist/services/release-dry-run.js +253 -0
  103. package/dist/services/release-evidence.js +138 -0
  104. package/dist/services/release-publish.js +163 -0
  105. package/dist/services/smoke-evidence.js +104 -0
  106. package/dist/services/task-read-model.js +125 -0
  107. package/dist/services/tools-list.js +26 -0
  108. package/dist/services/write-preflight.js +240 -0
  109. package/dist/task/task-capsule.js +121 -0
  110. package/dist/tools/fake-shell.js +56 -0
  111. package/dist/tui/cache.js +341 -0
  112. package/dist/tui/constants.js +44 -0
  113. package/dist/tui/layout.js +140 -0
  114. package/dist/tui/markdown.js +238 -0
  115. package/dist/tui/read-model-worker.js +24 -0
  116. package/dist/tui/read-model.js +502 -0
  117. package/dist/tui/snapshot.js +434 -0
  118. package/dist/tui/state.js +229 -0
  119. package/dist/tui/terminal.js +475 -0
  120. package/dist/tui/theme.js +86 -0
  121. package/package.json +16 -0
@@ -0,0 +1,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
+ }