@khoavu882/doflow 2.0.1 → 2.0.2

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 (93) hide show
  1. package/core/.claude-plugin/plugin.json +1 -1
  2. package/core/.codex-plugin/plugin.json +1 -1
  3. package/core/.plugin/plugin.json +1 -1
  4. package/core/harnesses/claude/hooks/session-end.sh +21 -1
  5. package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
  6. package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
  7. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +25 -1
  8. package/core/registry/contribution-points.json +1 -1
  9. package/core/registry/intake.json +3 -3
  10. package/core/registry/lifecycle.json +256 -5
  11. package/core/registry/packages/doflow/doflow-package.json +2 -2
  12. package/core/registry/settings.json +12 -12
  13. package/core/registry/storage.json +1 -2
  14. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
  15. package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
  16. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
  17. package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
  18. package/core/shared/scripts/doflow/bin/doflow-run +18 -0
  19. package/core/shared/skills/do-design/SKILL.md +6 -2
  20. package/core/shared/skills/do-design/references/data_model_example.md +167 -0
  21. package/core/shared/templates/doflow/data-model-template.md +136 -102
  22. package/package.json +1 -1
  23. package/src/adapters/antigravity/index.js +54 -12
  24. package/src/adapters/codex/agents.js +15 -1
  25. package/src/adapters/codex/config.js +5 -2
  26. package/src/adapters/copilot/index.js +11 -4
  27. package/src/adapters/copy-tree.js +7 -0
  28. package/src/adapters/created-file.js +41 -0
  29. package/src/adapters/mcp-entries.js +11 -1
  30. package/src/adapters/opencode/index.js +32 -5
  31. package/src/adapters/pi/index.js +15 -4
  32. package/src/adapters/pi/mcp.js +44 -18
  33. package/src/cli/commands/remove.js +3 -1
  34. package/src/cli/commands/update.js +7 -3
  35. package/src/cli/index.js +24 -12
  36. package/src/cli/runtime-commands.js +1 -1
  37. package/src/cli/shared.js +5 -3
  38. package/src/helper/marker-merge.js +11 -7
  39. package/src/install/backup-set.js +7 -3
  40. package/src/install/backup.js +13 -7
  41. package/src/install/context.js +2 -1
  42. package/src/install/manifest.js +35 -11
  43. package/src/install/paths.js +38 -0
  44. package/src/install/targets.js +11 -2
  45. package/src/lifecycle/index.js +80 -3
  46. package/src/lifecycle/retired-harnesses.js +16 -1
  47. package/src/lifecycle/view.js +3 -2
  48. package/src/registry/index.js +21 -4
  49. package/src/release/docs-blocks.js +5 -1
  50. package/src/runtime/advice/doctor.js +2 -2
  51. package/src/runtime/brief-tools.js +2 -1
  52. package/src/runtime/capability-router.js +26 -3
  53. package/src/runtime/context/pointer.js +17 -5
  54. package/src/runtime/context-pack.js +48 -8
  55. package/src/runtime/dispatch/integrate.js +27 -8
  56. package/src/runtime/dispatch/loop.js +7 -0
  57. package/src/runtime/dispatch/verify-run.js +9 -3
  58. package/src/runtime/feature-pointer.js +10 -1
  59. package/src/runtime/health.js +9 -4
  60. package/src/runtime/implementation-gate.js +10 -2
  61. package/src/runtime/intake/engine.js +11 -9
  62. package/src/runtime/knowledge/code-retrieval.js +11 -5
  63. package/src/runtime/knowledge/semble-provider.js +12 -2
  64. package/src/runtime/lifecycle/cli.js +7 -5
  65. package/src/runtime/lifecycle/overview.js +32 -2
  66. package/src/runtime/lifecycle/release.js +16 -3
  67. package/src/runtime/lifecycle/retention.js +12 -15
  68. package/src/runtime/lifecycle/store-upkeep.js +13 -3
  69. package/src/runtime/mask.js +3 -3
  70. package/src/runtime/mcp-consultations.js +11 -5
  71. package/src/runtime/research-request.js +3 -1
  72. package/src/runtime/review/command.js +3 -7
  73. package/src/runtime/review/start.js +24 -10
  74. package/src/runtime/roadmap/loop.js +14 -3
  75. package/src/runtime/roadmap/rows.js +2 -1
  76. package/src/runtime/settings/files.js +3 -1
  77. package/src/runtime/settings/prepared-run.js +12 -2
  78. package/src/runtime/settings/resolve.js +13 -1
  79. package/src/runtime/settings/write.js +73 -2
  80. package/src/runtime/storage/registry.js +2 -2
  81. package/src/runtime/storage/roots.js +4 -18
  82. package/src/runtime/storage/rules.js +22 -46
  83. package/src/runtime/storage/work-status.js +14 -4
  84. package/src/runtime/task-scope.js +17 -2
  85. package/src/runtime/task-state.js +11 -4
  86. package/src/runtime/trace/analyze.js +4 -2
  87. package/src/runtime/trace/indicators.js +25 -20
  88. package/src/runtime/trace/spans.js +4 -5
  89. package/src/runtime/verb-view.js +6 -5
  90. package/src/runtime/worker/command.js +4 -2
  91. package/src/runtime/worker/readiness.js +8 -4
  92. package/src/runtime/worker/supervisor.js +18 -6
  93. package/src/runtime/workflow-orchestrator.js +6 -2
@@ -31,16 +31,9 @@ function createPruneSettingsResolver({ roots, fsImpl = fs, onView } = {})
31
31
  if (!loadedView) {
32
32
  loadedView = true;
33
33
  try {
34
- let settingsRegistry;
35
- if (fsImpl !== fs) {
36
- const { loadSettingsRegistry } = require('../settings/registry.js');
37
- const loaded = loadSettingsRegistry({ fsImpl });
38
- if (!loaded.ok) throw new Error(loaded.detail);
39
- settingsRegistry = loaded.registry;
40
- }
41
- const { loadSettings } = require('../settings/resolve.js');
34
+ const { loadSettings, registryOn } = require('../settings/resolve.js');
42
35
  const projectRoot = roots['project-doflow'] ? path.dirname(roots['project-doflow']) : null;
43
- view = loadSettings({ projectRoot, env: resolverEnv, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
36
+ view = loadSettings({ projectRoot, env: resolverEnv, fsImpl, ...registryOn(fsImpl) });
44
37
  } catch {
45
38
  // A settings registry fault fails closed for every root, as it did for each old call.
46
39
  view = null;
@@ -69,15 +62,8 @@ function readPruneSetting({ roots, rootId, fsImpl = fs } = {})
69
62
  const resolverEnv = {};
70
63
  if (roots['config-home']) resolverEnv.XDG_CONFIG_HOME = path.dirname(roots['config-home']);
71
64
  try {
72
- let settingsRegistry;
73
- if (fsImpl !== fs) {
74
- const { loadSettingsRegistry } = require('../settings/registry.js');
75
- const loaded = loadSettingsRegistry({ fsImpl });
76
- if (!loaded.ok) throw new Error(loaded.detail);
77
- settingsRegistry = loaded.registry;
78
- }
79
- const { loadSettings } = require('../settings/resolve.js');
80
- const view = loadSettings({ projectRoot, global, env: resolverEnv, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
65
+ const { loadSettings, registryOn } = require('../settings/resolve.js');
66
+ const view = loadSettings({ projectRoot, global, env: resolverEnv, fsImpl, ...registryOn(fsImpl) });
81
67
  const resolved = view.get('storage.prune', global ? 'global' : 'project');
82
68
  return {
83
69
  setting: resolved.value === 'on' ? 'on' : 'off',
@@ -232,16 +232,15 @@ function lifecyclePathsSafe(project , fsImpl ) {
232
232
  }
233
233
  return true;
234
234
  }
235
- function definitelyMissing(fsImpl , target ) {
236
- try { fsImpl.lstatSync(target); return false; }
237
- catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR'; }
238
- }
239
235
  function reportCandidates(store , entries , context ) {
240
236
  const { fsImpl, storeDir, roots, now, settings, shouldStop } = context;
241
237
  const project = projectRoot(roots);
242
238
  const key = project && projectKey(project, fsImpl), selected = [];
243
239
  for (const entry of entries) {
244
240
  if (shouldStop?.()) throw budgetError();
241
+ // A folder under another project's key stays: ENOENT or ENOTDIR on its recorded root never
242
+ // authorises deleting it, since an unmounted or renamed root looks the same as a gone one.
243
+ if (entry.name !== key) continue;
245
244
  const folder = path.join(storeDir, entry.name);
246
245
  let children, bodyNames = [];
247
246
  try {
@@ -251,50 +250,27 @@ function reportCandidates(store , entries , context ) {
251
250
  if (BODY.test(name) && safeStat(fsImpl, path.join(folder, name))?.isFile()) bodyNames.push(name);
252
251
  }
253
252
  } catch (error ) { if (error.partial) throw error; continue; }
254
- if (entry.name === key) {
255
- const lifecycle = path.join(project, '.doflow/state/lifecycle');
256
- if (!safeStat(fsImpl, path.join(lifecycle, 'migrated.json'))?.isFile() || !lifecyclePathsSafe(project, fsImpl)) continue;
257
- let snapshot;
258
- try { snapshot = readEvents(project, { fsImpl, lockFallback: false, shouldStop }); }
259
- catch { continue; }
260
- if (snapshot.incomplete) throw budgetError();
261
- if (snapshot.unreadable.length) continue;
262
- const eventIds = new Set();
263
- for (const event of snapshot.events) {
264
- if (shouldStop?.()) throw budgetError();
265
- const ids = [event.data.id, ...(Array.isArray(event.data.ids) ? event.data.ids : [])];
266
- for (const id of ids) {
267
- if (shouldStop?.()) throw budgetError();
268
- if (typeof id === 'string' && /^FU-[0-9a-hjkmnp-tv-z]{6}$/.test(id)) eventIds.add(id);
269
- }
270
- }
271
- for (const body of bodyNames) {
272
- if (shouldStop?.()) throw budgetError();
273
- const stat = safeStat(fsImpl, path.join(folder, body));
274
- if (stat && !eventIds.has(body.slice(0, -4)) && now - stat.mtimeMs > settings.orphanGraceHours * HOUR)
275
- selected.push({ name: `${entry.name}/${body}`, reason: 'orphan-body', bytes: stat.size, mtimeMs: stat.mtimeMs });
276
- }
277
- } else {
278
- const marker = path.join(folder, '.root');
279
- if (!safeStat(fsImpl, marker)?.isFile() || !bodyNames.length) continue;
280
- let validFolder = true;
281
- for (const name of children) {
253
+ const lifecycle = path.join(project, '.doflow/state/lifecycle');
254
+ if (!safeStat(fsImpl, path.join(lifecycle, 'migrated.json'))?.isFile() || !lifecyclePathsSafe(project, fsImpl)) continue;
255
+ let snapshot;
256
+ try { snapshot = readEvents(project, { fsImpl, lockFallback: false, shouldStop }); }
257
+ catch { continue; }
258
+ if (snapshot.incomplete) throw budgetError();
259
+ if (snapshot.unreadable.length) continue;
260
+ const eventIds = new Set();
261
+ for (const event of snapshot.events) {
262
+ if (shouldStop?.()) throw budgetError();
263
+ const ids = [event.data.id, ...(Array.isArray(event.data.ids) ? event.data.ids : [])];
264
+ for (const id of ids) {
282
265
  if (shouldStop?.()) throw budgetError();
283
- if (name !== '.root' && !bodyNames.includes(name)) { validFolder = false; break; }
266
+ if (typeof id === 'string' && /^FU-[0-9a-hjkmnp-tv-z]{6}$/.test(id)) eventIds.add(id);
284
267
  }
285
- if (!validFolder) continue;
286
- let root;
287
- try { root = fsImpl.readFileSync(marker, 'utf8').trim(); } catch { continue; }
288
- if (!path.isAbsolute(root) || !definitelyMissing(fsImpl, root)) continue;
289
- let newest = -Infinity;
290
- for (const body of bodyNames) {
291
- if (shouldStop?.()) throw budgetError();
292
- const stat = safeStat(fsImpl, path.join(folder, body));
293
- if (!stat) { newest = Infinity; break; }
294
- newest = Math.max(newest, stat.mtimeMs);
295
- }
296
- if (now - newest > store.bound.foreignMaxAgeDays * DAY)
297
- selected.push({ name: entry.name, reason: 'foreign-root-gone', bytes: entry.stat.size, mtimeMs: newest });
268
+ }
269
+ for (const body of bodyNames) {
270
+ if (shouldStop?.()) throw budgetError();
271
+ const stat = safeStat(fsImpl, path.join(folder, body));
272
+ if (stat && !eventIds.has(body.slice(0, -4)) && now - stat.mtimeMs > settings.orphanGraceHours * HOUR)
273
+ selected.push({ name: `${entry.name}/${body}`, reason: 'orphan-body', bytes: stat.size, mtimeMs: stat.mtimeMs });
298
274
  }
299
275
  }
300
276
  return selected;
@@ -81,6 +81,17 @@ function isRunNamespaceDir(name ) {
81
81
  return safeSegment(name) && !name.endsWith('.lock');
82
82
  }
83
83
 
84
+ /**
85
+ * Whether a flat run is superseded by a namespaced copy: it names that copy's namespace as its own
86
+ * feature (`featureSlug`, else `slug`) and the copy has the same file name. A flat run that names
87
+ * no feature, or one whose namespace holds no copy, is a run of its own and is never dropped. The
88
+ * one ownership rule for every reader that walks `orchestration/` (health, trace, work status).
89
+ */
90
+ function isSupersededFlatRun(name , value , copies ) {
91
+ const own = stringField(value, 'featureSlug') || stringField(value, 'slug');
92
+ return own !== null && isRunNamespaceDir(own) && copies.some((copy) => copy.namespace === own && copy.name === name);
93
+ }
94
+
84
95
  /**
85
96
  * Every run id the orchestration store holds, flat or under a `<slug>/` namespace (115 C-1: a task id
86
97
  * other than its feature's slug lives in `orchestration/<slug>/`). `cache.runLocations` maps each id
@@ -136,9 +147,8 @@ function readRunAt(cache , runId , where ) {
136
147
  const file = path.join(stateRoot(cache.projectRoot), 'orchestration', ...(where ? [where] : []), `${runId}.json`);
137
148
  let result ;
138
149
  try {
139
- // A namespaced copy is a file base never read (N-4): a link, a FIFO or a device there reads as
140
- // unreadable (the record is kept) and never blocks. The flat copy keeps its plain read (FU-d88kp0).
141
- const text = where && typeof cache.fsImpl.lstatSync === 'function' && typeof cache.fsImpl.openSync === 'function'
150
+ // A link, a FIFO or a device there reads as unreadable (the record is kept) and never blocks.
151
+ const text = typeof cache.fsImpl.lstatSync === 'function' && typeof cache.fsImpl.openSync === 'function'
142
152
  ? require('../task-scope.js').readRegularFileText(file, cache.fsImpl) : cache.fsImpl.readFileSync(file, 'utf8');
143
153
  if (text === null) throw new Error('not a regular file');
144
154
  const value = JSON.parse(text);
@@ -542,4 +552,4 @@ function workStatus({
542
552
  return answer('finished', run ? 'run-finished' : 'no-run', resolvedRun.id, feature, since);
543
553
  }
544
554
 
545
- module.exports = { workStatus, recordExtension, createWorkCache, refreshedWorkCache, RUN_OPEN_STATES };
555
+ module.exports = { workStatus, recordExtension, createWorkCache, refreshedWorkCache, RUN_OPEN_STATES, isRunNamespaceDir, isSupersededFlatRun };
@@ -176,9 +176,23 @@ function readRegularFileText(file , fsImpl = fs) {
176
176
  }
177
177
  }
178
178
 
179
+ /** Whether `slug` is a register feature (it has `decisions/register.json`), for a record file under
180
+ * a checkout's `.doflow/state`. Such a feature never reads another feature's identity-less flat
181
+ * record (045 isolation). A file whose checkout cannot be told, or a feature that does not resolve,
182
+ * has no register. */
183
+ function featureHasRegister(file , slug ) {
184
+ const marker = `${path.sep}.doflow${path.sep}state${path.sep}`;
185
+ const at = file.lastIndexOf(marker);
186
+ if (at <= 0) return false;
187
+ try {
188
+ const found = resolveFeature({ projectRoot: file.slice(0, at), slug });
189
+ return !found.error && Boolean(found.featureDir) && fs.existsSync(path.join(found.featureDir, 'decisions', 'register.json'));
190
+ } catch { return false; }
191
+ }
192
+
179
193
  /**
180
194
  * Whether a flat record may be read as `slug`'s (D-3): it names that feature (`featureSlug` on a
181
- * run, `slug` on a readiness record) or names none. A file that does not parse names nobody, so a
195
+ * run, `slug` on a readiness record), or names none and `slug` has no decision register. A file that does not parse names nobody, so a
182
196
  * namespaced read never falls back to it. The probe never blocks and follows no link: a flat file
183
197
  * that is not a regular file of at most `MAX_RECORD_FILE_BYTES` is not read; `unsafeIsOurs` is then
184
198
  * the answer, so a caller whose reader refuses such a file (the run store's `readRunRecord`) is
@@ -198,7 +212,8 @@ function flatRecordIsOurs(file , slug , fsImpl = fs, u
198
212
  data = JSON.parse(text);
199
213
  } catch { return false; }
200
214
  const named = [data?.featureSlug, data?.slug].find((value) => typeof value === 'string' && value !== '');
201
- return named === undefined || named === slug;
215
+ if (named === undefined) return !slug || !featureHasRegister(file, slug);
216
+ return named === slug;
202
217
  }
203
218
 
204
219
  /**
@@ -376,10 +376,14 @@ function acquireLock(fsImpl , file , options = {})
376
376
  + strayEntries(fsImpl, lockDir));
377
377
  }
378
378
 
379
- /** Names a non-directory entry in the lock (no DoFlow runtime makes one; it blocks the lock until removed). */
379
+ /** The non-directory entries in the lock (no DoFlow runtime makes one; each blocks the lock until removed). */
380
+ function strayNames(fsImpl , lockDir ) {
381
+ try { return fsImpl.readdirSync(lockDir, { withFileTypes: true }).filter((d ) => !d.isDirectory()).map((d ) => d.name); } catch { return []; }
382
+ }
383
+
384
+ /** `strayNames` as the sentence the lock timeout carries. */
380
385
  function strayEntries(fsImpl , lockDir ) {
381
- let stray = [];
382
- try { stray = fsImpl.readdirSync(lockDir, { withFileTypes: true }).filter((d ) => !d.isDirectory()).map((d ) => d.name); } catch { return ''; }
386
+ const stray = strayNames(fsImpl, lockDir);
383
387
  if (stray.length === 0) return '';
384
388
  return ` It holds ${stray.map((name) => `'${name}'`).join(', ')}, which no DoFlow runtime makes and which keeps it locked: `
385
389
  + `remove ${stray.length > 1 ? 'them' : 'it'} once no DoFlow process is running.`;
@@ -389,7 +393,8 @@ function strayEntries(fsImpl , lockDir ) {
389
393
  * Takes the cross-process lock for one state file with a bounded wait.
390
394
  * Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5); each try is one
391
395
  * tryAcquireLock, which waits at most 40 ms, and only on a process that reached the lock at the same moment.
392
- * Throws an Error('store-locked') after one try plus retries.
396
+ * Throws an Error('store-locked') after one try plus retries, carrying `lockPath` (the lock folder) and
397
+ * `strayEntries` (the names in it that no DoFlow runtime makes) as properties.
393
398
  * @param {Object} fsImpl
394
399
  * @param {string} file
395
400
  * @param {Object} [options]
@@ -420,6 +425,8 @@ function boundedAcquire(
420
425
  }
421
426
  const error = new Error('store-locked');
422
427
  (error ).code = 'store-locked';
428
+ (error ).lockPath = `${file}.lock`;
429
+ (error ).strayEntries = strayNames(fsImpl, `${file}.lock`);
423
430
  throw error;
424
431
  }
425
432
 
@@ -115,8 +115,10 @@ function latencyHotspots(scopes , th , latency ) {
115
115
  const groups = new Map ();
116
116
  for (const s of sc.spans) {
117
117
  if (!Number.isInteger(s.durationMs) || !ranItself(s)) continue;
118
- const key = `${s.kind}\n${s.name ?? ''}`;
119
- if (!groups.has(key)) groups.set(key, { kind: s.kind, name: s.name ?? null, spans: [] });
118
+ // Every derived `test` span is named 'test'; the command's class tells unrelated shell commands apart.
119
+ const name = s.kind === 'test' && typeof s.attrs?.test_class === 'string' ? s.attrs.test_class : (s.name ?? null);
120
+ const key = `${s.kind}\n${name ?? ''}`;
121
+ if (!groups.has(key)) groups.set(key, { kind: s.kind, name, spans: [] });
120
122
  groups.get(key).spans.push(s);
121
123
  }
122
124
  for (const g of [...groups.values()].sort((a, b) => cmp(a.kind, b.kind) || cmp(a.name ?? '', b.name ?? ''))) {
@@ -40,27 +40,33 @@ function parseAt(value ) {
40
40
  return Number.isNaN(ms) ? null : ms;
41
41
  }
42
42
 
43
- /** Every record in a directory, plus the names of the ones that did not parse. A file that cannot be
44
- * read is counted and named rather than skipped: a report that quietly drops what it could not read
45
- * overstates its own coverage, which is the defect `malformedLines` already guards against for runs. */
46
- function readRecords(dir , fsImpl = fs) {
47
- if (!fsImpl.existsSync(dir)) return { records: [], unreadable: [], exists: false };
48
- const records = [];
43
+ /** Every run the `list` action shows (flat and namespaced, this checkout and the other ones, each
44
+ * logical run once), plus the ones that could not be read, named with a fixed reason. A file that
45
+ * cannot be read is counted and named rather than skipped: a report that quietly drops what it could
46
+ * not read overstates its own coverage, which is the defect `malformedLines` already guards against
47
+ * for runs. The reason is never the error's own text, which can carry a path or file bytes. */
48
+ function readRecords(root ) {
49
+ const { orchestrationRuns, runEntriesAt, selectRunEntries, runStoreDir } = require('../implementation-gate.js');
50
+ const listed = orchestrationRuns({ stateRoot: root });
49
51
  const unreadable = [];
50
- for (const name of fsImpl.readdirSync(dir).sort()) {
51
- if (!name.endsWith('.json')) continue;
52
- try {
53
- const parsed = JSON.parse(fsImpl.readFileSync(path.join(dir, name), 'utf8'));
54
- if (!parsed || typeof parsed !== 'object' || !parsed.taskId) {
55
- unreadable.push({ file: name, reason: 'not an orchestration record' });
56
- continue;
57
- }
58
- records.push(parsed);
59
- } catch (err ) {
60
- unreadable.push({ file: name, reason: err.message });
52
+ let entries ;
53
+ if (listed.ok) entries = selectRunEntries(listed.entries);
54
+ else {
55
+ entries = selectRunEntries(runEntriesAt(root, 'current'));
56
+ unreadable.push({ file: '(other checkouts)', reason: 'could not be listed' });
57
+ }
58
+ const records = [];
59
+ for (const entry of entries) {
60
+ const name = entry.origin === 'other' ? entry.file : path.relative(runStoreDir(entry.root), entry.file);
61
+ if (entry.error) {
62
+ unreadable.push({ file: name, reason: entry.error === 'io' ? 'not a regular file' : 'not valid JSON' });
63
+ } else if (!entry.record || typeof entry.record !== 'object' || !entry.record.taskId) {
64
+ unreadable.push({ file: name, reason: 'not an orchestration record' });
65
+ } else {
66
+ records.push(entry.record);
61
67
  }
62
68
  }
63
- return { records, unreadable, exists: true };
69
+ return { records, unreadable, exists: entries.length > 0 || fs.existsSync(orchestrationDir(root)) };
64
70
  }
65
71
 
66
72
  /** A stage row is "not work done" when the orchestrator backfilled it or imported it rather than
@@ -196,8 +202,7 @@ function printClass(group ) {
196
202
  }
197
203
 
198
204
  function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {}) {
199
- const dir = orchestrationDir(projectRoot);
200
- const read = readRecords(dir);
205
+ const read = readRecords(projectRoot);
201
206
  const view = buildIndicators(read);
202
207
 
203
208
  if (json) {
@@ -9,6 +9,7 @@ const { resolveRunsLocation, normalizeRunRecord } = require('./ledger.js');
9
9
  const { SAFE_TOKEN } = require('./telemetry.js');
10
10
  const { finish } = require('./render.js');
11
11
  const { REPO_ROOT } = require('../../helper/repo-root.js');
12
+ const { isRunNamespaceDir, isSupersededFlatRun } = require('../storage/work-status.js');
12
13
 
13
14
  /**
14
15
  * `trace spans` (088 IC-011, FR-005, DD-5): spans projected offline from the records that already
@@ -63,7 +64,6 @@ const { REPO_ROOT } = require('../../helper/repo-root.js');
63
64
 
64
65
  const PARTITION = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
65
66
  const STORE_FILE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.json$/;
66
- const NAMESPACE_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
67
67
  const HEX16 = /^[0-9a-f]{16}$/;
68
68
  const HEX32 = /^[0-9a-f]{32}$/;
69
69
  const WORKER_RUN_ID = /^w-[0-9a-f]{12}$/;
@@ -201,15 +201,14 @@ function readStores(fsImpl , stateDir , rel , gaps , kin
201
201
  if (!namespaced) return out.map(({ file, value }) => ({ file, value }));
202
202
  let entries = [];
203
203
  try { entries = fsImpl.readdirSync(base, { withFileTypes: true }); } catch { entries = []; }
204
- const namespaces = entries.filter((entry ) => entry.isDirectory() && NAMESPACE_DIR.test(entry.name) && !entry.name.endsWith('.lock'))
204
+ const namespaces = entries.filter((entry ) => entry.isDirectory() && isRunNamespaceDir(entry.name))
205
205
  .map((entry ) => entry.name).sort();
206
206
  for (const namespace of namespaces) {
207
207
  const dir = path.join(base, namespace);
208
208
  for (const name of listDir(fsImpl, dir, STORE_FILE)) read(`${rel}/${namespace}/${name}`, name, dir, namespace);
209
209
  }
210
- const copied = (flat ) => out.some((other) => other.namespace !== ''
211
- && other.name === flat.name && (typeof flat.value.featureSlug !== 'string' || flat.value.featureSlug === other.namespace));
212
- return out.filter((entry) => entry.namespace !== '' || !copied(entry)).map(({ file, value }) => ({ file, value }));
210
+ const copies = out.filter((other) => other.namespace !== '');
211
+ return out.filter((entry) => entry.namespace !== '' || !isSupersededFlatRun(entry.name, entry.value, copies)).map(({ file, value }) => ({ file, value }));
213
212
  }
214
213
 
215
214
  const objects = (v ) => (v && typeof v === 'object' && !Array.isArray(v)
@@ -87,7 +87,8 @@ function projectValue(value , node ) {
87
87
  let part;
88
88
  if (child.array) {
89
89
  part = Array.isArray(value[key])
90
- ? value[key].map((element ) => (child.node.all ? element : (projectValue(element, child.node) ?? {})))
90
+ ? value[key].map((element ) => (child.node.all || element === null || typeof element !== 'object' || Array.isArray(element)
91
+ ? element : (projectValue(element, child.node) ?? {})))
91
92
  : undefined;
92
93
  } else {
93
94
  part = projectValue(value[key], child.node);
@@ -115,7 +116,7 @@ const DIGITS = /^[0-9]+$/;
115
116
 
116
117
  function encodeCursor(key ) {
117
118
  if (!Number.isSafeInteger(key.atMs) || key.atMs < 0 || String(key.atMs).length > 15) {
118
- throw new TypeError(`cursor time must be 0 to 15 digits of milliseconds (got ${key.atMs})`);
119
+ throw new ViewUsageError(`cursor time must be 0 to 15 digits of milliseconds (got ${key.atMs})`);
119
120
  }
120
121
  return `${key.atMs}:${key.event}`;
121
122
  }
@@ -197,8 +198,8 @@ function commandLine(verb , action , carried
197
198
  * are those the spec's `carry` names, in that order, then `--cursor`, `--limit`, `--view` (a `page` line names
198
199
  * `--view full` only when the caller's `view` is `full`). Returns no line when
199
200
  * the kind has nothing to point at (`page` without a nextCursor, `page-full` with nothing shown). In a JSON answer
200
- * (`json: true`) the `page-full` and `full` lines end with `--json`, so the command as written prints the full
201
- * payload; a text answer's lines carry no `--json`.
201
+ * (`json: true`) every line ends with `--json`, so the command as written prints the same JSON shape; a text
202
+ * answer's lines carry no `--json`.
202
203
  */
203
204
  function viewLines(kind , ctx
204
205
 
@@ -222,7 +223,7 @@ function viewLines(kind , ctx
222
223
  } else {
223
224
  args.view = 'full';
224
225
  }
225
- const asJson = ctx.json === true && kind !== 'page' ? ' --json' : '';
226
+ const asJson = ctx.json === true ? ' --json' : '';
226
227
  return [`${LABELS[kind]}: ${commandLine(ctx.spec.verb, ctx.spec.action, args)}${asJson}`];
227
228
  }
228
229
 
@@ -285,7 +285,7 @@ function evaluate({ reg, worker, harness, needs, coverage, env, cwd, root, nowMs
285
285
  nowMs,
286
286
  );
287
287
  const requirements = readiness.requirementsFor({ harness, worker }, facts, needs, coverage, edit);
288
- return { requirements, version: found.version, ...readiness.eligibility(requirements) };
288
+ return { requirements, version: found.version, binaryPath: found.path, ...readiness.eligibility(requirements) };
289
289
  }
290
290
 
291
291
  /** IC-011 steps 0 to 12, then the supervisor spawn and the start poll. */
@@ -365,7 +365,9 @@ async function runAction(o ) {
365
365
  edit: editRecord, childOf, request: childOf ? o.request : null,
366
366
  ...launchRecord(worker, identity, prompt.bytes.toString('utf8'), edit, binding),
367
367
  ...(binding ? { binding: { model: binding.model, flag: binding.flag } } : {}),
368
- descriptor: worker,
368
+ // The binary the probe found, as an absolute path: the supervisor spawns from the execution worktree,
369
+ // where a relative PATH entry would name another file than the one probed.
370
+ descriptor: evaluated.binaryPath && worker?.cli ? { ...worker, cli: { ...worker.cli, binary: evaluated.binaryPath } } : worker,
369
371
  prompt: { path: prompt.path, sha256: prompt.sha256, bytes: prompt.bytes.length },
370
372
  needs: evaluated.requirements.map((item ) => ({ requirement: item.requirement, at: item.at ?? null })),
371
373
  };
@@ -78,9 +78,13 @@ function workerDescriptors({ descriptors, registry, view } = {})
78
78
  return ids.filter((id) => Object.hasOwn(descriptors, id)).map((id) => ({ harness: id, worker: descriptors[id] }));
79
79
  }
80
80
 
81
- /** The first executable `binary` on `env.PATH`, or null. Starts no process. */
82
- function findOnPath(binary , env , platform = process.platform) {
83
- const dirs = String(env?.PATH ?? '').split(path.delimiter).filter(Boolean);
81
+ /**
82
+ * The first executable `binary` on `env.PATH`, or null. Starts no process. A relative PATH entry is
83
+ * read against `cwd` (the caller's directory) when one is given, so the path found is absolute and
84
+ * names the same file whichever directory a later process starts in.
85
+ */
86
+ function findOnPath(binary , env , platform = process.platform, cwd ) {
87
+ const dirs = String(env?.PATH ?? '').split(path.delimiter).filter(Boolean).map((dir) => (cwd ? path.resolve(cwd, dir) : dir));
84
88
  const suffixes = platform === 'win32'
85
89
  ? ['', ...String(env?.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]
86
90
  : [''];
@@ -116,7 +120,7 @@ function discoverOne({ harness, worker } , { env, cwd, now, platform } )
116
120
  if (!supported(cli)) {
117
121
  return { harness, binary: null, present: false, path: null, version: null, reason: 'cli-unknown', at: iso(now()) };
118
122
  }
119
- const found = findOnPath(cli.binary, env, platform);
123
+ const found = findOnPath(cli.binary, env, platform, cwd);
120
124
  if (!found) return { harness, binary: cli.binary, present: false, path: null, version: null, reason: 'not-on-path', at: iso(now()) };
121
125
  const execFileImpl = (bin , args , options ) => execFileSync(bin, args, { ...options, env, cwd });
122
126
  const result = runProbe([found, ...cli.versionArgv], { execFileImpl, timeoutMs: PROBE_TIMEOUT_MS });
@@ -321,22 +321,33 @@ function boundedWait(promise , ms ) {
321
321
  return Promise.race([promise, bound]).finally(() => clearTimeout(timer));
322
322
  }
323
323
 
324
- /** Reads only the bounded Codex output-last-message prefix; absent or unreadable files are optional. */
325
- function readLastMessage(parser , file ) {
326
- if (typeof parser.lastMessage !== 'function') return;
324
+ /** What masking did to a retained final message, in the run record beside the stdout and stderr pointers. */
325
+ const ALTERED_WARNING = 'masking altered this final message: it is not suitable for byte-exact patch application; apply the worker\'s changes from its isolated worktree, not from this text';
326
+
327
+ /**
328
+ * Reads only the bounded Codex output-last-message prefix; absent or unreadable files are optional.
329
+ * Returns the pointer the run record keeps (`path` relative to `stateRoot`, the masked `bytes`,
330
+ * whether masking `altered` the text and, when it did, the `warning`), or null when no file was read.
331
+ */
332
+ function readLastMessage(parser , file , stateRoot ) {
333
+ if (typeof parser.lastMessage !== 'function') return null;
327
334
  let fd ;
328
335
  try {
329
336
  fd = fs.openSync(file, 'r');
330
337
  const bytes = Buffer.alloc(65536);
331
338
  const length = fs.readSync(fd, bytes, 0, bytes.length, 0);
332
- const masked = maskBody(bytes.subarray(0, length).toString('utf8')).text;
339
+ const raw = bytes.subarray(0, length).toString('utf8');
340
+ const masked = maskBody(raw).text;
333
341
  parser.lastMessage(masked);
334
342
  // NFR-004: the retained copy holds only the masked, bounded text, never the raw worker output.
335
343
  fs.closeSync(fd);
336
344
  fd = undefined;
337
- try { fs.writeFileSync(file, masked, 'utf8'); } catch { fs.rmSync(file, { force: true }); }
345
+ try { fs.writeFileSync(file, masked, 'utf8'); } catch { fs.rmSync(file, { force: true }); return null; }
346
+ const altered = masked !== raw;
347
+ return { path: path.relative(stateRoot, file).split(path.sep).join('/'), bytes: Buffer.byteLength(masked), altered, ...(altered ? { warning: ALTERED_WARNING } : {}) };
338
348
  } catch {
339
349
  // Codex may not have produced this optional file; that does not change the run outcome.
350
+ return null;
340
351
  } finally {
341
352
  if (fd !== undefined) {
342
353
  try { fs.closeSync(fd); } catch { /* unreadable/closed files are ignored */ }
@@ -509,7 +520,8 @@ async function superviseRun({
509
520
  readOnly = { status: 'unverifiable', changes: [], head: { before: before.head, after: null }, refsChanged: null };
510
521
  }
511
522
  }
512
- if (lastMessageFile) readLastMessage(parser, lastMessageFile);
523
+ const lastMessage = lastMessageFile ? readLastMessage(parser, lastMessageFile, stateRoot) : null;
524
+ if (lastMessage) (output ).lastMessage = lastMessage;
513
525
  const report = parser.end();
514
526
  // 106 IC-002: a failure whose masked summary holds a declared quota signal is marked quota.
515
527
  const quota = quotaOf(report, run.descriptor);
@@ -484,9 +484,13 @@ class WorkflowOrchestrator {
484
484
  },
485
485
  harnessName: displayName,
486
486
  since,
487
- addCommand: (server ) => {
487
+ addCommand: (server , harnessId = null) => {
488
488
  const reach = reachOf();
489
- const row = reach?.harnesses.find((item ) => !['selected', 'configured'].includes(item.servers[server]));
489
+ const has = (item ) => ['selected', 'configured'].includes(item.servers[server]);
490
+ // The brief's own harness is where the server was missing; another harness having it says nothing.
491
+ const target = harnessId ? reach?.harnesses.find((item ) => item.id === harnessId) : null;
492
+ if (target) return has(target) ? null : reach.addCommand(target.id, target.scope, server);
493
+ const row = reach?.harnesses.find((item ) => !has(item));
490
494
  return row ? reach.addCommand(row.id, row.scope, server) : null;
491
495
  },
492
496
  });