@khoavu882/doflow 2.0.0 → 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 (176) hide show
  1. package/README.md +4 -5
  2. package/bin/doflow.js +1 -1
  3. package/core/.claude-plugin/plugin.json +1 -1
  4. package/core/.codex-plugin/plugin.json +1 -1
  5. package/core/.plugin/plugin.json +1 -1
  6. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
  8. package/core/harnesses/claude/hooks/session-end.sh +21 -1
  9. package/core/harnesses/claude/settings/settings.json +8 -9
  10. package/core/harnesses/codex/hooks/hooks.json +0 -10
  11. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
  12. package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
  13. package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
  14. package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
  15. package/core/harnesses/copilot/hooks/doflow.json +0 -6
  16. package/core/harnesses/kiro/agents/doflow.json +0 -10
  17. package/core/harnesses/kiro/hooks/doflow.json +0 -10
  18. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
  19. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
  20. package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
  21. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
  22. package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
  23. package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
  24. package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
  25. package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
  26. package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
  27. package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
  28. package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
  29. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
  30. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
  31. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
  32. package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
  33. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +26 -2
  34. package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
  35. package/core/registry/contracts.json +1 -1
  36. package/core/registry/contribution-points.json +5 -2
  37. package/core/registry/gates.json +5 -5
  38. package/core/registry/harnesses.json +9 -9
  39. package/core/registry/intake.json +53 -0
  40. package/core/registry/lifecycle.json +176 -503
  41. package/core/registry/package-templates/intake-category/files/category.json +8 -0
  42. package/core/registry/package-templates/intake-category/template.json +4 -0
  43. package/core/registry/packages/doflow/doflow-package.json +2 -2
  44. package/core/registry/rules.json +8 -8
  45. package/core/registry/settings.json +12 -16
  46. package/core/registry/storage.json +9 -2
  47. package/core/registry/telemetry.json +1 -1
  48. package/core/registry/verification.json +0 -5
  49. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
  50. package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
  51. package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
  52. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
  53. package/core/shared/guidance/rules/RULES.md +2 -2
  54. package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
  55. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
  56. package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
  57. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
  58. package/core/shared/scripts/doflow/bin/doflow-run +23 -2
  59. package/core/shared/skills/do/SKILL.md +3 -3
  60. package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
  61. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
  62. package/core/shared/skills/do-code-review/SKILL.md +7 -4
  63. package/core/shared/skills/do-design/SKILL.md +11 -5
  64. package/core/shared/skills/do-design/references/data_model_example.md +167 -0
  65. package/core/shared/skills/do-diagnose/SKILL.md +4 -2
  66. package/core/shared/skills/do-document/SKILL.md +1 -0
  67. package/core/shared/skills/do-document/references/embedded-research.md +1 -1
  68. package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
  69. package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
  70. package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
  71. package/core/shared/skills/do-flow/SKILL.md +4 -4
  72. package/core/shared/skills/do-flow/references/task_classes.md +5 -5
  73. package/core/shared/skills/do-implement/SKILL.md +9 -7
  74. package/core/shared/skills/do-plan/SKILL.md +9 -6
  75. package/core/shared/templates/doflow/data-model-template.md +136 -102
  76. package/core/shared/workflows/do-flow-chain.md +2 -0
  77. package/package.json +1 -1
  78. package/src/adapters/antigravity/index.js +54 -12
  79. package/src/adapters/claude/index.js +23 -2
  80. package/src/adapters/codex/agents.js +15 -1
  81. package/src/adapters/codex/config.js +5 -2
  82. package/src/adapters/codex/hooks.js +39 -3
  83. package/src/adapters/codex/index.js +13 -0
  84. package/src/adapters/copilot/index.js +11 -4
  85. package/src/adapters/copy-tree.js +8 -2
  86. package/src/adapters/created-file.js +41 -0
  87. package/src/adapters/mcp-entries.js +11 -1
  88. package/src/adapters/opencode/index.js +32 -5
  89. package/src/adapters/pi/index.js +15 -4
  90. package/src/adapters/pi/mcp.js +44 -18
  91. package/src/cli/commands/remove.js +3 -1
  92. package/src/cli/commands/status.js +1 -1
  93. package/src/cli/commands/update.js +7 -3
  94. package/src/cli/index.js +43 -19
  95. package/src/cli/runtime-commands.js +13 -5
  96. package/src/cli/shared.js +5 -3
  97. package/src/helper/marker-merge.js +11 -7
  98. package/src/install/backup-set.js +7 -3
  99. package/src/install/backup.js +13 -7
  100. package/src/install/context.js +2 -1
  101. package/src/install/manifest.js +35 -11
  102. package/src/install/paths.js +38 -0
  103. package/src/install/targets.js +11 -2
  104. package/src/lifecycle/coverage.js +1 -30
  105. package/src/lifecycle/hook-tier.js +2 -2
  106. package/src/lifecycle/index.js +80 -3
  107. package/src/lifecycle/retired-harnesses.js +16 -1
  108. package/src/lifecycle/view.js +3 -2
  109. package/src/registry/index.js +21 -4
  110. package/src/release/docs-blocks.js +37 -53
  111. package/src/runtime/advice/doctor.js +2 -2
  112. package/src/runtime/brief-tools.js +2 -1
  113. package/src/runtime/capability-router.js +26 -3
  114. package/src/runtime/context/pointer.js +84 -17
  115. package/src/runtime/context-pack.js +52 -9
  116. package/src/runtime/dispatch/integrate.js +27 -8
  117. package/src/runtime/dispatch/loop.js +7 -0
  118. package/src/runtime/dispatch/verify-run.js +9 -3
  119. package/src/runtime/evidence-ledger.js +18 -0
  120. package/src/runtime/feature-pointer.js +10 -1
  121. package/src/runtime/health.js +68 -5
  122. package/src/runtime/implementation-gate.js +14 -6
  123. package/src/runtime/intake/engine.js +679 -0
  124. package/src/runtime/intake/index.js +136 -0
  125. package/src/runtime/intake/manifest.js +140 -0
  126. package/src/runtime/intake/taxonomy.js +206 -0
  127. package/src/runtime/knowledge/code-retrieval.js +11 -5
  128. package/src/runtime/knowledge/semble-provider.js +12 -2
  129. package/src/runtime/lifecycle/cli.js +76 -6
  130. package/src/runtime/lifecycle/followup.js +32 -6
  131. package/src/runtime/lifecycle/overview.js +34 -3
  132. package/src/runtime/lifecycle/release.js +16 -3
  133. package/src/runtime/lifecycle/retention.js +12 -15
  134. package/src/runtime/lifecycle/store-upkeep.js +13 -3
  135. package/src/runtime/mask.js +3 -3
  136. package/src/runtime/mcp-consultations.js +11 -5
  137. package/src/runtime/packages/points.js +10 -0
  138. package/src/runtime/research-request.js +3 -1
  139. package/src/runtime/review/command.js +3 -7
  140. package/src/runtime/review/start.js +24 -10
  141. package/src/runtime/roadmap/attendance.js +137 -0
  142. package/src/runtime/roadmap/gates.js +6 -3
  143. package/src/runtime/roadmap/loop.js +127 -33
  144. package/src/runtime/roadmap/merge.js +17 -8
  145. package/src/runtime/roadmap/rows.js +15 -5
  146. package/src/runtime/roadmap/seed.js +153 -41
  147. package/src/runtime/roadmap/worktree-cleanup.js +147 -0
  148. package/src/runtime/settings/files.js +3 -1
  149. package/src/runtime/settings/prepared-run.js +12 -2
  150. package/src/runtime/settings/registry.js +1 -1
  151. package/src/runtime/settings/resolve.js +13 -1
  152. package/src/runtime/settings/write.js +73 -2
  153. package/src/runtime/slots.js +1 -1
  154. package/src/runtime/storage/registry.js +2 -2
  155. package/src/runtime/storage/roots.js +4 -18
  156. package/src/runtime/storage/rules.js +22 -46
  157. package/src/runtime/storage/work-status.js +14 -4
  158. package/src/runtime/task-classifier.js +39 -4
  159. package/src/runtime/task-scope.js +17 -2
  160. package/src/runtime/task-state.js +13 -7
  161. package/src/runtime/trace/analyze.js +4 -2
  162. package/src/runtime/trace/indicators.js +25 -20
  163. package/src/runtime/trace/spans.js +4 -5
  164. package/src/runtime/verb-view.js +268 -0
  165. package/src/runtime/verification/engine.js +0 -45
  166. package/src/runtime/worker/command.js +4 -2
  167. package/src/runtime/worker/launch.js +5 -37
  168. package/src/runtime/worker/readiness.js +10 -6
  169. package/src/runtime/worker/supervisor.js +18 -6
  170. package/src/runtime/workflow-orchestrator.js +39 -7
  171. package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
  172. package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
  173. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
  174. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
  175. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
  176. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
@@ -15,14 +15,40 @@ const followup = require('./followup.js');
15
15
  const { StoreUnsafeError, StoreLockedError } = require('./event-store.js');
16
16
  const { prepareStore } = require('./store-upkeep.js');
17
17
  const { printSafe } = require('../mask.js');
18
- const { buildOverview, initFeature, featureStatus, activeFeature } = require('./overview.js');
18
+ const { buildOverview, capOverviewBrief, initFeature, featureStatus, activeFeature } = require('./overview.js');
19
19
  const { recordFeature, clearSlug } = require('../feature-pointer.js');
20
20
  const { releaseFeatures, recordMerged } = require('./release.js');
21
21
  const goal = require('./goal.js');
22
+ const { ViewUsageError, resolveView, projectBrief, viewLines, withEnvelope, serializeJson } = require('../verb-view.js');
22
23
 
23
24
  const FOLLOWUP_ACTIONS = ['add', 'list', 'take', 'settle', 'promote', 'report'];
24
25
  const LIFECYCLE_ACTIONS = ['overview', 'init', 'release', 'status', 'merged'];
25
26
  const GOAL_ACTIONS = ['add', 'item', 'check', 'link', 'list', 'done'];
27
+ /** The brief `followup --action list` answer (feature 137, IC-004): the fields that let a caller choose an item. */
28
+ const FOLLOWUP_LIST_BRIEF = Object.freeze({
29
+ verb: 'followup',
30
+ action: 'list',
31
+ carry: Object.freeze(['state', 'ids', 'limit']),
32
+ keep: Object.freeze([
33
+ '$.ok', '$.action', '$.state', '$.count', '$.items[].id', '$.items[].state', '$.items[].statement',
34
+ '$.conflicts', '$.unreadable', '$.unreadableReasons',
35
+ ]),
36
+ });
37
+ /** The brief `lifecycle --action overview` answer (feature 137, IC-006): keep paths, so a new full field is dropped until named here. */
38
+ const OVERVIEW_BRIEF = Object.freeze({
39
+ verb: 'lifecycle',
40
+ action: 'overview',
41
+ carry: Object.freeze(['maintain', 'since']),
42
+ keep: Object.freeze([
43
+ '$.ok', '$.mode', '$.releaseMode', '$.integrationRef', '$.gitError', '$.followups.open', '$.followups.shown',
44
+ '$.followups.items[].id', '$.followups.items[].statement', '$.followups.items[].source', '$.followups.items[].pending',
45
+ '$.intents', '$.goals[].goal', '$.goals[].outcome', '$.goals[].items', '$.goals[].proposeDone', '$.goals[].nudges',
46
+ '$.goals[].conflicts', '$.conflicts', '$.unreadable', '$.unreadableReasons', '$.failures', '$.reason', '$.note',
47
+ '$.integrationBehind', '$.pending', '$.omitted', '$.next',
48
+ ]),
49
+ });
50
+ /** A brief list answers one page of this many items unless `--limit` says otherwise. */
51
+ const BRIEF_PAGE = 10;
26
52
  /** The flags each goal action takes, besides --goal, --channel and --json; any other one given is a usage error. */
27
53
  const GOAL_FLAGS = {
28
54
  add: ['statement', 'item'], item: ['text'], check: ['item', 'evidence', 'unmet'],
@@ -40,6 +66,7 @@ function followupLines(result ) {
40
66
  case 'list':
41
67
  if (result.items.length === 0) lines.push(`No ${result.state === 'all' ? '' : `${result.state} `}follow-ups.`);
42
68
  for (const i of result.items) lines.push(`${i.id} ${i.state}${i.takenBy ? ` by ${i.takenBy}` : ''}${i.intent ? ` -> ${i.intent}` : ''} (${sourceText(i.source)}, ${i.added}) ${i.statement}`);
69
+ for (const line of result.next) lines.push(`next: ${line}`);
43
70
  break;
44
71
  case 'take': lines.push(`${result.feature} took ${result.ids.join(', ')}`); break;
45
72
  case 'settle': lines.push(`settled ${result.ids.join(', ')} as ${result.as}`); break;
@@ -129,7 +156,7 @@ function goalLines(r ) {
129
156
 
130
157
  /** Prints a result and sets the exit status; shared by the verbs. */
131
158
  function emit(result , json , lines ) {
132
- if (json) console.log(JSON.stringify(result, null, 2));
159
+ if (json) console.log(serializeJson(result));
133
160
  else if (result.ok === false) console.log(printSafe(`${result.finding}: ${result.message}`));
134
161
  else for (const line of lines(result)) console.log(printSafe(line));
135
162
  return finishRuntime(result.ok === false ? 1 : 0);
@@ -140,7 +167,7 @@ function guarded(verb , action , json , fn ) {
140
167
  try {
141
168
  return fn();
142
169
  } catch (error ) {
143
- if (error instanceof followup.FollowupUsageError || error instanceof StoreUnsafeError) return usageError(verb, error.message, json);
170
+ if (error instanceof followup.FollowupUsageError || error instanceof ViewUsageError || error instanceof StoreUnsafeError) return usageError(verb, error.message, json);
144
171
  if (error instanceof StoreLockedError) return emit({ ok: false, action, finding: 'store-locked', message: error.message }, json, () => []);
145
172
  throw error;
146
173
  }
@@ -157,6 +184,29 @@ function refuseGlobal(verb , global , json ) {
157
184
  return global ? usageError(verb, `${verb} works on the project store at the repository root and refuses -g`, json) : null;
158
185
  }
159
186
 
187
+ /**
188
+ * Prints a `followup --action list` answer in the view it asked for (IC-004). Brief projects the page onto the keep
189
+ * list; a full call with no cursor and no limit is the service object, untouched. Every other answer carries the
190
+ * envelope and the next lines. Text mode shows the page's items in the usual format and the next lines only when
191
+ * the page is not the whole list.
192
+ */
193
+ function emitList(result , view , flags , json ) {
194
+ if (result.ok === false) return emit(result, json, followupLines);
195
+ const paged = result.shown !== undefined;
196
+ const carried = { state: flags.state, ids: flags.ids, limit: flags.limit };
197
+ const lines = !paged ? [] : [
198
+ ...viewLines('page', { spec: FOLLOWUP_LIST_BRIEF, carried, nextCursor: result.nextCursor, view, json: Boolean(json) }),
199
+ ...(view === 'brief' ? viewLines('page-full', { spec: FOLLOWUP_LIST_BRIEF, carried, cursor: flags.cursor, shown: result.shown, json: Boolean(json) }) : []),
200
+ ];
201
+ if (!json) return emit({ ...result, next: result.shown < result.count ? lines : [] }, false, followupLines);
202
+ if (view === 'brief') {
203
+ const body = projectBrief(result, FOLLOWUP_LIST_BRIEF.keep);
204
+ return emit(withEnvelope(body, { view, shown: result.shown, nextCursor: result.nextCursor, next: lines }), true, followupLines);
205
+ }
206
+ if (!paged) return emit(result, true, followupLines);
207
+ return emit(withEnvelope(result, { view, shown: result.shown, nextCursor: result.nextCursor, next: lines }), true, followupLines);
208
+ }
209
+
160
210
  /**
161
211
  * `followup` verb. Actions: add, list, take, settle, promote, report.
162
212
  * @param {Object} options
@@ -175,6 +225,12 @@ function handleFollowupCommand({ action, cwd, global = false, slug = null, json
175
225
  if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action report only`);
176
226
  }
177
227
  }
228
+ if (action !== 'list') {
229
+ for (const name of ['view', 'cursor', 'limit']) {
230
+ if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action list only`);
231
+ }
232
+ }
233
+ const view = action === 'list' ? resolveView(flags.view, 'brief') : null;
178
234
  for (const name of ['tag', 'confirm', 'exclude']) {
179
235
  if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to the lifecycle verb's --action release only, not to followup`);
180
236
  }
@@ -198,7 +254,9 @@ function handleFollowupCommand({ action, cwd, global = false, slug = null, json
198
254
  defaults: { stage: flags.stage, slug, source: flags.source, taskClass: flags.taskClass, taskId: flags.taskId, release: flags.release, batch },
199
255
  });
200
256
  } else if (action === 'list') {
201
- result = followup.listFollowups({ root, state: flags.state });
257
+ return emitList(followup.listFollowups({
258
+ root, state: flags.state, ids: flags.ids, cursor: flags.cursor, limit: flags.limit, defaultLimit: view === 'brief' ? BRIEF_PAGE : null,
259
+ }), view, flags, json);
202
260
  } else if (action === 'take') {
203
261
  result = followup.takeFollowups({ root, ids: flags.ids, slug, channel: flags.channel });
204
262
  } else if (action === 'settle') {
@@ -230,13 +288,25 @@ function handleLifecycleCommand({ action, cwd, global = false, slug = null, json
230
288
  if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action release only`);
231
289
  }
232
290
  }
291
+ if (act !== 'overview' && flags.view !== undefined && flags.view !== false) throw new followup.FollowupUsageError('--view applies to --action overview only');
292
+ const view = act === 'overview' ? resolveView(flags.view, 'full') : null;
233
293
  if (flags.reason !== undefined && act !== 'merged') throw new followup.FollowupUsageError('--reason applies to --action merged only');
234
294
  const root = projectRoot(cwd || process.cwd());
235
295
  const refusedStore = upkeep('lifecycle', act, root);
236
296
  if (refusedStore) return emit(refusedStore, json, lifecycleLines);
237
297
  let result;
238
- if (act === 'overview') result = buildOverview({ root, maintain: Boolean(flags.maintain), since: flags.since });
239
- else if (act === 'init') {
298
+ if (act === 'overview') {
299
+ result = buildOverview({ root, maintain: Boolean(flags.maintain), since: flags.since });
300
+ // The text answer is the same in both views (IC-001); only the JSON brief is cut to one page of items. Maintain
301
+ // keeps its 50: that loop settles every pending item. The next lines name the paged list and carry no full-view line.
302
+ if (view === 'brief' && json) {
303
+ const items = flags.maintain ? result.followups.items : result.followups.items.slice(0, BRIEF_PAGE);
304
+ const brief = capOverviewBrief({ ...result, followups: { ...result.followups, shown: items.length, items } });
305
+ // Something cut from a list: the next lines also say how to get everything.
306
+ const next = brief.omitted ? [...result.next, ...viewLines('full', { spec: OVERVIEW_BRIEF, carried: { maintain: flags.maintain, since: flags.since }, json: true })] : undefined;
307
+ return emit(withEnvelope(projectBrief(brief, OVERVIEW_BRIEF.keep), { view, ...(next ? { next } : {}) }), true, lifecycleLines);
308
+ }
309
+ } else if (act === 'init') {
240
310
  result = initFeature({ root, slug, take: flags.take, intent: flags.intent, goal: flags.goal });
241
311
  if (result.ok) recordFeature({ storeRoot: root, cwd: cwd || process.cwd(), slug });
242
312
  }
@@ -22,6 +22,7 @@ const { STAGES: CHAIN_STAGES } = require('../decision-register.js');
22
22
  const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
23
23
  const { resolveActiveFeature } = require('../feature-resolve.js');
24
24
  const { SETTLE_AS, withDerivedDone } = require('./fold.js');
25
+ const { pageItems } = require('../verb-view.js');
25
26
  const { appendEvents, readFold, byFromChannel, randomChars } = require('./event-store.js');
26
27
  const { deriveStatuses } = require('./status.js');
27
28
  const { writeIntent, kebabTitle } = require('./intent-writer.js');
@@ -334,12 +335,37 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
334
335
 
335
336
  // ── list ───────────────────────────────────────────────────────────────────────────────────────
336
337
 
337
- /** IC-006 `list`: `state` is open (default), taken, done, dismissed or all. */
338
- function listFollowups({ root, state = 'open', hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
339
- if (state !== 'all' && !STATES.includes(state)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${state}')`);
338
+ /** The most items one list answer holds, however large a `--limit` asks for (IC-004). */
339
+ const LIST_MAX_LIMIT = 100;
340
+
341
+ /** The key the fold orders on and a list cursor names: the creating event's time in ms, then its event id (IC-008). */
342
+ function followupOrderKey(item ) {
343
+ const created = item.history[0];
344
+ return { atMs: Date.parse(created.at), event: created.event };
345
+ }
346
+
347
+ /**
348
+ * IC-006 `list`: `state` is open (default), taken, done, dismissed or all. `ids` keeps the named items (and, with no
349
+ * `state`, looks in every state); an id that is no item at all is the `unknown-id` refusal. With `cursor`, `limit` or
350
+ * `defaultLimit` the answer holds one page of `items` plus `shown` and `nextCursor`; with none it is the whole list.
351
+ */
352
+ function listFollowups({ root, state, ids, cursor, limit, defaultLimit = null, hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
353
+ const wanted = ids === undefined ? null : parseIds(ids);
354
+ const wantedState = state === undefined ? (wanted ? 'all' : 'open') : state;
355
+ if (wantedState !== 'all' && !STATES.includes(wantedState)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${wantedState}')`);
340
356
  const { fold, followups } = loadFollowups(root, { hasBody, fsImpl, statuses, now });
341
- const items = state === 'all' ? followups : followups.filter((item ) => item.state === state);
342
- return { ok: true, action: 'list', state, count: items.length, items, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
357
+ let matched = followups;
358
+ if (wanted) {
359
+ const known = new Set(followups.map((item ) => item.id));
360
+ const unknown = wanted.filter((id) => !known.has(id));
361
+ if (unknown.length) return refusal('list', 'unknown-id', `${unknown.join(', ')} ${unknown.length === 1 ? 'is' : 'are'} not a follow-up in this store. Nothing was listed.`);
362
+ matched = followups.filter((item ) => wanted.includes(item.id));
363
+ }
364
+ if (wantedState !== 'all') matched = matched.filter((item ) => item.state === wantedState);
365
+ const answer = { ok: true, action: 'list', state: wantedState, count: matched.length, items: matched, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
366
+ if (cursor === undefined && limit === undefined && defaultLimit === null) return answer;
367
+ const page = pageItems(matched, { cursor, limit, defaultLimit, maxLimit: LIST_MAX_LIMIT, key: followupOrderKey });
368
+ return { ...answer, items: page.items, shown: page.shown, nextCursor: page.nextCursor };
343
369
  }
344
370
 
345
371
  // ── take, settle, promote ──────────────────────────────────────────────────────────────────────
@@ -431,7 +457,7 @@ function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl
431
457
  }
432
458
 
433
459
  module.exports = {
434
- addFollowups, reportFollowup, listFollowups, takeFollowups, settleFollowups, promoteFollowups,
460
+ addFollowups, reportFollowup, listFollowups, followupOrderKey, takeFollowups, settleFollowups, promoteFollowups,
435
461
  loadFollowups, readBatchFile, resolveFeatureSlug, parseIds, oneLine, channelBy,
436
462
  FollowupUsageError, STAGES, STATES, SOURCE_KINDS, STATEMENT_MAX,
437
463
  };
@@ -17,6 +17,7 @@ const { appendEvents, readFold, readEvents } = require('./event-store.js');
17
17
  const { deriveStatuses, bucketize, behindNote } = require('./status.js');
18
18
  const { FollowupUsageError, parseIds } = require('./followup.js');
19
19
  const { describeActive } = require('../feature-pointer.js');
20
+ const { commandLine } = require('../verb-view.js');
20
21
 
21
22
  /** The failure store, or null when its module cannot be loaded: the overview then reports no failures. */
22
23
  function failureStoreOrNull() {
@@ -27,6 +28,8 @@ const DISCOVERY_SHOWN = 15;
27
28
  const MAINTAIN_SHOWN = 50;
28
29
  /** Slugs shown per finished or awaiting-release bucket: those lists only grow, and the overview runs on every brainstorm. */
29
30
  const FEATURES_SHOWN = 10;
31
+ /** Entries per list in the JSON brief overview: intents, the ids of one intent, the nudges of one goal (137 FU-2jtkch). */
32
+ const BRIEF_LIST_SHOWN = 10;
30
33
 
31
34
  const refusal = (action , finding , message ) => ({ ok: false, action, finding, message });
32
35
 
@@ -115,7 +118,10 @@ function goalView(goal , features , statuses , conflicts = [])
115
118
  const met = goal.items.filter((i ) => i.met).length;
116
119
  const nudges = [];
117
120
  if (buckets.finished.length) {
118
- const who = buckets.finished.length === 1 ? `linked feature ${buckets.finished[0]} is finished` : `linked features ${buckets.finished.join(', ')} are finished`;
121
+ // The nudge names the most recent finished features only; `features.finished` still lists every one.
122
+ const finished = shownBucket(buckets.finished);
123
+ const who = buckets.finished.length === 1 ? `linked feature ${buckets.finished[0]} is finished`
124
+ : `linked features ${finished.shown.join(', ')}${finished.more ? ` (+${finished.more} more)` : ''} are finished`;
119
125
  for (const item of goal.items.filter((i ) => !i.met)) nudges.push(`${item.id} is unchecked and ${who}`);
120
126
  }
121
127
  return {
@@ -130,7 +136,7 @@ function goalView(goal , features , statuses , conflicts = [])
130
136
 
131
137
  function nextLines({ shown, open, items, intents, goals, maintain, pendingItems, failures } ) {
132
138
  const next = [];
133
- if (shown < open) next.push('List the rest: doflow-run followup --action list');
139
+ if (shown < open) next.push(`Page through all ${open} open follow-ups: ${commandLine('followup', 'list', {})}`);
134
140
  if (items.length) next.push(`Take items into the new feature when its folder exists: doflow-run lifecycle --action init --slug <slug> --take ${items[0].id}`);
135
141
  if (intents.length) {
136
142
  next.push(`Start from a promoted intent: /do-brainstorm --intent ${intents[0].path}, then doflow-run lifecycle --action init --slug <slug> --intent ${intents[0].path}`);
@@ -159,6 +165,31 @@ function nextLines({ shown, open, items, intents, goals, maintain, pendingItems,
159
165
  return next;
160
166
  }
161
167
 
168
+ /**
169
+ * The JSON brief form of an overview answer: at most BRIEF_LIST_SHOWN intents, ids per intent and nudges per goal. What
170
+ * was cut is counted in `omitted` (absent when nothing was cut): `intents`, and `intentItems`, `nudges` and
171
+ * `finished` (the finished features a goal's nudges leave out of their text) per intent or goal that exceeds the
172
+ * bound. The full view lists everything.
173
+ */
174
+ function capOverviewBrief(result ) {
175
+ const omitted = {};
176
+ const intents = result.intents.slice(0, BRIEF_LIST_SHOWN).map((intent ) => {
177
+ if (intent.items.length > BRIEF_LIST_SHOWN) {
178
+ (omitted.intentItems ??= []).push({ path: intent.path, total: intent.items.length, more: intent.items.length - BRIEF_LIST_SHOWN });
179
+ }
180
+ return { ...intent, items: intent.items.slice(0, BRIEF_LIST_SHOWN) };
181
+ });
182
+ if (result.intents.length > BRIEF_LIST_SHOWN) omitted.intents = { total: result.intents.length, more: result.intents.length - BRIEF_LIST_SHOWN };
183
+ const goals = result.goals.map((goal ) => {
184
+ const finished = goal.features.finished.length;
185
+ if (finished > FEATURES_SHOWN) (omitted.finished ??= []).push({ goal: goal.goal, total: finished, more: finished - FEATURES_SHOWN });
186
+ if (goal.nudges.length <= BRIEF_LIST_SHOWN) return goal;
187
+ (omitted.nudges ??= []).push({ goal: goal.goal, total: goal.nudges.length, more: goal.nudges.length - BRIEF_LIST_SHOWN });
188
+ return { ...goal, nudges: goal.nudges.slice(0, BRIEF_LIST_SHOWN) };
189
+ });
190
+ return { ...result, intents, goals, ...(Object.keys(omitted).length ? { omitted } : {}) };
191
+ }
192
+
162
193
  /**
163
194
  * IC-007.
164
195
  * @param {Object} options
@@ -312,4 +343,4 @@ function activeFeature({ root, cwd, now = new Date(), fsImpl = nodeFs } ) {
312
343
  return derived.ok ? { ...derived, active } : { ...result, slug: active.slug, message: `${active.slug} is tracked by its run only; \`lifecycle --action init\` to track it.` };
313
344
  }
314
345
 
315
- module.exports = { buildOverview, initFeature, featureStatus, activeFeature, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
346
+ module.exports = { buildOverview, capOverviewBrief, initFeature, featureStatus, activeFeature, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
@@ -39,6 +39,7 @@ const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
39
39
  const { appendEvents, readFold } = require('./event-store.js');
40
40
  const { deriveStatuses, readGitFacts, behindNote, subjectNames, readMerges, readBranchTips, readHistory } = require('./status.js');
41
41
  const { FollowupUsageError, oneLine, channelBy } = require('./followup.js');
42
+ const { parseSemVer, compareSemVer } = require('../../helper/semver.js');
42
43
 
43
44
  /** The release-tag pattern of IC-020, the one `git-state` filters by. */
44
45
  const RELEASE_TAG = /^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/;
@@ -125,13 +126,25 @@ function releaseFeatures({ root, tag, confirm = false, features, exclude, channe
125
126
  return refusal('release', 'untracked-feature', `${untracked.join(', ')} ${untracked.length === 1 ? 'is' : 'are'} not tracked, so ${untracked.length === 1 ? 'it' : 'they'} cannot be added to or excluded from a release; run doflow-run lifecycle --action init --slug <slug> first. Nothing was written.`);
126
127
  }
127
128
 
128
- // DEC-030: containment in the tag being recorded never finishes a feature before its record exists.
129
- const otherTags = { ...facts, release_tags: facts.release_tags.filter((name ) => name !== tag) };
129
+ // DEC-030: containment in the tag being recorded never finishes a feature before its record exists. Of the
130
+ // other tags only an unrecorded one that is stable and earlier than `tag` (lower version, or a strict ancestor of it)
131
+ // may take a feature out of this release: a pre-release (v1.0.0-rc.1 before v1.0.0) or a later tag
132
+ // (v2.0.0 when recording v1.0.0) does not. A tag with a record is left in: status reads it by its record.
133
+ const tagCommit = commitOf(root, `refs/tags/${tag}`);
134
+ const recordedTags = new Set(fold.releases.map((r ) => r.tag));
135
+ const shadows = (name ) => {
136
+ const parsed = parseSemVer(name.slice(1));
137
+ if (!parsed || parsed.prerelease.length > 0) return false;
138
+ if (compareSemVer(name.slice(1), tag.slice(1)) < 0) return true;
139
+ // A strict ancestor only: a higher-versioned tag on the very same commit is not earlier.
140
+ const commit = tagCommit && commitOf(root, `refs/tags/${name}`);
141
+ return Boolean(commit) && commit !== tagCommit && isAncestor(root, commit, tagCommit);
142
+ };
143
+ const otherTags = { ...facts, release_tags: facts.release_tags.filter((name ) => name !== tag && (recordedTags.has(name) || shadows(name))) };
130
144
  const derived = deriveStatuses({ root, fold, facts: otherTags });
131
145
  if (derived.releaseMode === 'unknown' || !derived.integrationSha) return refusal('release', 'no-integration-ref', `${derived.reason || `${facts.integration_ref} does not resolve to a commit`}. Nothing was written.`);
132
146
 
133
147
  // X: the tag's commit once it exists, else the commit the status view pinned the integration ref to (DEC-044).
134
- const tagCommit = commitOf(root, `refs/tags/${tag}`);
135
148
  const bound = tagCommit || derived.integrationSha;
136
149
  const previousTag = previousTagOf(root, { tag, facts, bound });
137
150
  // A hotfix merged only into the tag's own history has no evidence on the integration chain: derive against the tag as well.
@@ -18,7 +18,7 @@
18
18
 
19
19
  const nodeFs = require('node:fs');
20
20
  const { isDeepStrictEqual } = require('node:util');
21
- const { loadSettings } = require('../settings/resolve.js');
21
+ const { loadSettings, registryOn } = require('../settings/resolve.js');
22
22
  const { foldInto, finalize, EVENT_TYPES } = require('./fold.js');
23
23
 
24
24
  const HOUR_MS = 3600000;
@@ -53,14 +53,7 @@ function retentionWindow({ env = process.env, projectRoot, fsImpl = nodeFs }
53
53
  const parsed = parseWindow(env);
54
54
  if (parsed.state === 'invalid') return parsed;
55
55
  try {
56
- let settingsRegistry;
57
- if (fsImpl !== nodeFs) {
58
- const { loadSettingsRegistry } = require('../settings/registry.js');
59
- const loaded = loadSettingsRegistry({ fsImpl });
60
- if (!loaded.ok) throw new Error(loaded.detail);
61
- settingsRegistry = loaded.registry;
62
- }
63
- const view = loadSettings({ projectRoot, env, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
56
+ const view = loadSettings({ projectRoot, env, fsImpl, ...registryOn(fsImpl) });
64
57
  const retention = view.get('storage.retentionHours', 'project');
65
58
  if (retention.state === 'invalid') return { state: 'invalid', raw: invalidRaw(retention, env) };
66
59
  if (retention.explicit) {
@@ -136,10 +129,12 @@ function unitsOf(events , shouldStop ) {
136
129
  }
137
130
 
138
131
  /**
139
- * Every string value and every object key in the data of the events whose type this version's fold
140
- * does not know (a newer DoFlow wrote them). The fold ignores them, so retention cannot tell what they change; a unit that
141
- * one of them names by id is kept whole rather than deleted from under it. Exact matches only:
142
- * keeping too little is a lost record, keeping too much only a file that stays.
132
+ * Every string value in the envelope, and every object key inside it, of the events whose type this
133
+ * version's fold does not know (a newer DoFlow wrote them). The fold ignores them, so retention
134
+ * cannot tell what they change; a unit that one of them names, by item id, event id or event file
135
+ * name, is kept whole rather than deleted from under it. The envelope's own field names are its
136
+ * shape, not references. Exact matches only: keeping too little is a lost record, keeping too much
137
+ * only a file that stays.
143
138
  * @returns {Set<string>|null} null when `shouldStop` fired
144
139
  */
145
140
  function unknownTypeStrings(events , shouldStop ) {
@@ -148,7 +143,7 @@ function unknownTypeStrings(events , shouldStop )
148
143
  for (const event of events) {
149
144
  if (shouldStop?.()) return null;
150
145
  if (known.has(event.type)) continue;
151
- const pending = [event.data];
146
+ const pending = Object.values(event);
152
147
  while (pending.length > 0) {
153
148
  if (shouldStop?.()) return null;
154
149
  const value = pending.pop();
@@ -260,7 +255,9 @@ function selectExpired(events , { now, windowMs, shouldStop } = {})
260
255
  };
261
256
  const baseSelected = safeSubset(candidates);
262
257
  if (!baseSelected) return incomplete();
263
- const unprotected = baseSelected.filter((unit ) => ![...unit.items].some((key ) => mentioned.has(key.slice(key.indexOf(':') + 1))));
258
+ const namedByUnknown = (unit ) => [...unit.items].some((key ) => mentioned.has(key.slice(key.indexOf(':') + 1)))
259
+ || unit.events.some((event ) => mentioned.has(event.id) || mentioned.has(`${event.id}.json`));
260
+ const unprotected = baseSelected.filter((unit ) => !namedByUnknown(unit));
264
261
  // Dropping units can leave a set the refold no longer accepts; check it again, inside the subset.
265
262
  const selected = unprotected.length === baseSelected.length ? baseSelected : safeSubset(unprotected);
266
263
  if (!selected) return incomplete();
@@ -342,9 +342,9 @@ function retainLocked(root , options = {}) {
342
342
  if (!release) return outcome(0, [], true);
343
343
  try {
344
344
  const swept = sweepStaleTmp(fsImpl, root, now, staleTmpMs, { maxFiles, shouldStop });
345
- if (swept.partial) return outcome(0, [], true);
345
+ if (swept.partial) return outcome(swept.removed, [], true);
346
346
  const result = retain(fsImpl, root, window, now, { maxFiles: Math.max(0, maxFiles - swept.removed), shouldStop });
347
- return outcome(result.removedCount, result.warning, result.partial, result.selected, result.code);
347
+ return outcome(swept.removed + result.removedCount, result.warning, result.partial, result.selected, result.code);
348
348
  } finally {
349
349
  release();
350
350
  }
@@ -358,6 +358,16 @@ function needsPrune(fsImpl , root , window , now ) {
358
358
  return selectExpired(readEvents(root, { fsImpl }).events, { now, windowMs: window.ms }).files.length > 0;
359
359
  }
360
360
 
361
+ /**
362
+ * Whether the old `agent-docs/lifecycle` copy still waits to be copied. Read-only (no lock, no
363
+ * write, `DOFLOW_UPKEEP` not consulted), for `doctor`; an unreadable folder counts as not pending.
364
+ */
365
+ function migrationPending(root , { fsImpl = nodeFs } = {}) {
366
+ try {
367
+ return !hasMarker(fsImpl, root) && !storeHasEvents(fsImpl, root) && legacyEventFiles(fsImpl, root).length > 0;
368
+ } catch { return false; }
369
+ }
370
+
361
371
  /**
362
372
  * Prepares the project store for one verb invocation.
363
373
  * @param {string} root projectRoot() result
@@ -439,4 +449,4 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
439
449
  }
440
450
  }
441
451
 
442
- module.exports = { prepareStore, retainLocked, removedLine, STALE_TMP_MS, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
452
+ module.exports = { prepareStore, migrationPending, retainLocked, removedLine, STALE_TMP_MS, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
@@ -12,8 +12,8 @@
12
12
  * Rules run in the table's order. Normalisation (rules 10 to 13) is a separate step applied to
13
13
  * failure messages only, after masking.
14
14
  *
15
- * The module has no I/O and no state; the home directory is read from `HOME` unless the caller
16
- * passes one.
15
+ * The module has no I/O and no state; the home directory is read from `HOME` (`USERPROFILE` when
16
+ * `HOME` is absent or empty, as on Windows) unless the caller passes one.
17
17
  */
18
18
 
19
19
  const MASKED = '<masked>';
@@ -152,7 +152,7 @@ function escapeRegExp(text ) { return text.replace(/[.*+?^${}()|[
152
152
 
153
153
  /** The home directory to abbreviate: an absolute path other than the file system root. */
154
154
  function homeOf(options ) {
155
- const raw = options && 'home' in options ? options.home : process.env.HOME;
155
+ const raw = options && 'home' in options ? options.home : (process.env.HOME || process.env.USERPROFILE);
156
156
  if (typeof raw !== 'string' || !(raw.startsWith('/') || /^[A-Za-z]:[\\/]/.test(raw))) return null;
157
157
  const home = raw.replace(/[\\/]+$/, '');
158
158
  return home.length > 1 ? home : null;
@@ -230,14 +230,15 @@ const bySortKey = (a , b ) => (a < b ? -1 : a > b ? 1 : 0);
230
230
  * Injected readers (all optional; a missing one contributes nothing):
231
231
  * - `ledger.queryEvidence({taskId})` -> evidence items (active ones count)
232
232
  * - `requests.list(taskId)` -> research requests
233
- * - `briefRecords` -> IC-018 records already parsed, each `{ runTaskId, stage, tasks, expected }`
233
+ * - `briefRecords` -> IC-018 records already parsed, each `{ runTaskId, stage, createdAt, tasks, expected }`
234
234
  * - `readReport(path)` -> report text, or null when the file is missing
235
235
  * - `reach.anyHarnessHas(server)` -> true | false | null (unknown); never decides an outcome
236
236
  * - `reach.harnessWith(server)` -> the display name of a harness that has the server, or null
237
237
  * - `harnessName(id)` -> a harness id's display name (for a brief record's `harness`)
238
- * - `addCommand(server)` -> the text after "add it:" in the unavailable notice
238
+ * - `addCommand(server, harnessId)` -> the text after "add it:" in the unavailable notice; `harnessId` is the harness
239
+ * of the brief that found the server unreachable, or null when no brief names one
239
240
  * - `since` -> ISO time of the previous stage's completion (or the run start); rule 3 counts only
240
- * evidence observed after it
241
+ * evidence observed after it, and only brief records created after it
241
242
  *
242
243
  * @returns {{ record: object, warnings: object[], notices: object[], evidenceToAdd: object[] }}
243
244
  */
@@ -266,8 +267,11 @@ function resolveConsultations({
266
267
  // ingested (design A6).
267
268
  const toolsUsed = [];
268
269
  const fromReports = new Map ();
270
+ // A stage visited twice leaves briefs from the earlier visit: when `since` is known only a brief created
271
+ // after it counts, and a record with no creation time (written before it was stamped) cannot be placed.
269
272
  const records = (Array.isArray(briefRecords) ? briefRecords : [])
270
- .filter((record ) => record && stageId && taskId && record.stage === stageId && record.runTaskId === taskId);
273
+ .filter((record ) => record && stageId && taskId && record.stage === stageId && record.runTaskId === taskId
274
+ && (since === null || (typeof record.createdAt === 'string' && record.createdAt > since)));
271
275
  const reports = [];
272
276
  for (const record of records) {
273
277
  for (const task of record.tasks || []) {
@@ -347,10 +351,12 @@ function resolveConsultations({
347
351
  // brief writes `unreachable` only where every config the harness merges was read (IC-017).
348
352
  const unreachable = new Map ();
349
353
  const reachableOn = new Map ();
354
+ const unreachableOn = new Map ();
350
355
  for (const record of records) {
351
356
  for (const expected of record.expected || []) {
352
357
  if (!expected || typeof expected.server !== 'string') continue;
353
358
  if (expected.reach === 'unreachable' && !unreachable.has(expected.server)) unreachable.set(expected.server, record.tasks?.[0]?.id);
359
+ if (expected.reach === 'unreachable' && !unreachableOn.has(expected.server)) unreachableOn.set(expected.server, record.harness ?? null);
354
360
  if (expected.reach === 'reachable' && !reachableOn.has(expected.server)) reachableOn.set(expected.server, record.harness ?? null);
355
361
  }
356
362
  }
@@ -415,7 +421,7 @@ function resolveConsultations({
415
421
  text: `[WARN] mcp: ${server} reported unavailable but ${has} has it configured`,
416
422
  });
417
423
  }
418
- const add = !has && typeof addCommand === 'function' ? addCommand(server) : null;
424
+ const add = !has && typeof addCommand === 'function' ? addCommand(server, unreachableOn.get(server) ?? null) : null;
419
425
  notices.push({
420
426
  kind: 'unavailable', server,
421
427
  text: `[INFO] mcp: ${server} unavailable to ${skill}${add ? `; add it: ${add}` : ''}`,
@@ -42,6 +42,15 @@ function presetValidator(kind ) {
42
42
  };
43
43
  }
44
44
 
45
+ /** An intake category contributed by a package (136 IC-006): IC-005's category shape, whose `id` is the contribution id. */
46
+ function intakeCategoryValidator(id , entry , context = {}) {
47
+ const { validateCategory } = require('../intake/taxonomy.js');
48
+ const location = context.packageId ? `package '${context.packageId}' intake category '${id}'` : `intake category '${id}'`;
49
+ const errors = validateCategory(entry, location);
50
+ if (entry && typeof entry === 'object' && !Array.isArray(entry) && entry.id !== id) errors.push(`${location}.id must equal the contribution id '${id}'`);
51
+ return errors;
52
+ }
53
+
45
54
  function workerValidator(id , worker , context = {}) {
46
55
  const errors = [];
47
56
  const location = context.packageId ? `package '${context.packageId}' worker '${id}'` : `worker '${id}'`;
@@ -168,6 +177,7 @@ const POINT_VALIDATORS
168
177
  'agent-v1': agentValidator,
169
178
  'worker-v1': workerValidator,
170
179
  'capability-provider-v1': capabilityProviderValidator,
180
+ 'intake-category-v1': intakeCategoryValidator,
171
181
  });
172
182
 
173
183
  function isObject(value ) {
@@ -95,7 +95,9 @@ class ResearchRequestStore {
95
95
  }
96
96
 
97
97
  stageFor(taskId , stageId ) {
98
- const run = readTaskState(this.fsImpl, this.runFile(taskId));
98
+ // The run is read the way every run read is: no link followed, no FIFO or device opened (FU-d88kp0).
99
+ const { readRunRecord } = require('./implementation-gate.js');
100
+ const run = readRunRecord(this.fsImpl, this.runFile(taskId), path.join(this.projectRoot, '.doflow', 'state', 'orchestration'));
99
101
  if (!run || run.taskClass !== 'feature' || run.state !== 'RUNNING') {
100
102
  throw new Error(`Task '${taskId}' has no active feature stage`);
101
103
  }
@@ -25,15 +25,10 @@ const TELEMETRY_COUNT_KEYS = Object.freeze([
25
25
  'plausible', 'unverified_blockers', 'bug_classes',
26
26
  ]);
27
27
 
28
- // Inherited location variables would point the check at another repository (C-8).
29
- const GIT_LOCATION_KEYS = Object.freeze([
30
- 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_COMMON_DIR',
31
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
32
- ]);
33
-
34
28
  function defaultGit(args , cwd , env ) {
35
29
  const gitEnv = { ...env };
36
- for (const key of GIT_LOCATION_KEYS) delete gitEnv[key];
30
+ // Inherited location variables would point the check at another repository (C-8); the list is verification's.
31
+ for (const key of require('../dispatch/verify-run.js').REPO_ENV) delete gitEnv[key];
37
32
  return spawnSync('git', ['-C', cwd, ...args], { cwd, env: gitEnv, stdio: 'ignore' });
38
33
  }
39
34
 
@@ -112,6 +107,7 @@ function handleReviewCommand({
112
107
  }
113
108
  return handleReviewStart({ slug: hasValue(slug) ? slug : null, base: hasValue(base) ? base : null, preset: hasValue(preset) ? preset : null, json, cwd, env, resolve, adviceClient, out });
114
109
  }
110
+ if (Array.isArray(recordPath)) return usage('--path may be given only once', json, out);
115
111
  if (hasValue(base)) return usage('--base is valid only with --action start', json, out);
116
112
  if (hasValue(preset)) return usage('--preset is valid only with --action start', json, out);
117
113
  if (hasValue(slug) && hasValue(recordPath)) return usage('use either --slug or --path, not both', json, out);