@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5

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 (236) hide show
  1. package/bin/doflow.js +2 -2
  2. package/core/.antigravity-plugin/plugin.json +0 -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 +7 -17
  7. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  8. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  9. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  10. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  11. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  12. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  13. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  15. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  16. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  17. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  18. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  19. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  20. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  22. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  23. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  24. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  26. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  27. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  28. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  29. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  30. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  31. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  32. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  33. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  34. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  35. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  36. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  37. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  38. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  39. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  40. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  41. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  42. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  43. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  44. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  45. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  46. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  47. package/core/registry/assets.json +19 -0
  48. package/core/registry/contracts.json +2 -2
  49. package/core/registry/harnesses.json +196 -10
  50. package/core/registry/lifecycle.json +2626 -7
  51. package/core/registry/storage.json +110 -83
  52. package/core/registry/verification.json +1 -1
  53. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
  54. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  55. package/core/shared/skills/do-design/SKILL.md +6 -6
  56. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  57. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  58. package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
  59. package/core/shared/skills/do-implement/SKILL.md +3 -2
  60. package/core/shared/skills/do-plan/SKILL.md +7 -8
  61. package/package.json +4 -2
  62. package/src/adapters/antigravity/{index.ts → index.js} +141 -105
  63. package/src/adapters/claude/{index.ts → index.js} +63 -49
  64. package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
  65. package/src/adapters/codex/{agents.ts → agents.js} +24 -24
  66. package/src/adapters/codex/{config.ts → config.js} +49 -49
  67. package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
  68. package/src/adapters/codex/{index.ts → index.js} +148 -105
  69. package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
  70. package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
  71. package/src/adapters/copilot/{index.ts → index.js} +42 -42
  72. package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
  73. package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
  74. package/src/adapters/gemini/{index.ts → index.js} +96 -57
  75. package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
  76. package/src/adapters/{index.ts → index.js} +13 -13
  77. package/src/adapters/kiro/{index.ts → index.js} +49 -35
  78. package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
  79. package/src/adapters/opencode/{index.ts → index.js} +52 -52
  80. package/src/adapters/pi/hooks.js +119 -0
  81. package/src/adapters/pi/{index.ts → index.js} +64 -39
  82. package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
  83. package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
  84. package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
  85. package/src/cli/commands/{install.ts → install.js} +25 -25
  86. package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
  87. package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
  88. package/src/cli/commands/{remove.ts → remove.js} +10 -10
  89. package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
  90. package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
  91. package/src/cli/commands/{status.ts → status.js} +24 -24
  92. package/src/cli/commands/{tools.ts → tools.js} +16 -16
  93. package/src/cli/commands/{update.ts → update.js} +20 -20
  94. package/src/cli/{index.ts → index.js} +43 -31
  95. package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
  96. package/src/cli/{shared.ts → shared.js} +40 -40
  97. package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
  98. package/src/helper/{git.ts → git.js} +2 -2
  99. package/src/helper/{guardrails.ts → guardrails.js} +10 -10
  100. package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
  101. package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
  102. package/src/helper/{prompt.ts → prompt.js} +5 -5
  103. package/src/helper/{repo-root.ts → repo-root.js} +2 -2
  104. package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
  105. package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
  106. package/src/helper/{toml.ts → toml.js} +48 -48
  107. package/src/install/{backup-set.ts → backup-set.js} +16 -16
  108. package/src/install/{backup.ts → backup.js} +65 -65
  109. package/src/install/{context.ts → context.js} +15 -15
  110. package/src/install/{manifest.ts → manifest.js} +27 -27
  111. package/src/install/{mcp.ts → mcp.js} +30 -30
  112. package/src/install/{paths.ts → paths.js} +15 -15
  113. package/src/install/{targets.ts → targets.js} +10 -10
  114. package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
  115. package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
  116. package/src/lifecycle/coverage.js +57 -0
  117. package/src/lifecycle/hook-step.js +61 -0
  118. package/src/lifecycle/{index.ts → index.js} +160 -146
  119. package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
  120. package/src/lifecycle/{policies.ts → policies.js} +11 -11
  121. package/src/lifecycle/{view.ts → view.js} +28 -28
  122. package/src/registry/{index.ts → index.js} +195 -66
  123. package/src/release/build-package.js +91 -0
  124. package/src/release/changelog-section.js +70 -0
  125. package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
  126. package/src/runtime/advice/{cli.ts → cli.js} +31 -31
  127. package/src/runtime/advice/{client.ts → client.js} +44 -44
  128. package/src/runtime/advice/{config.ts → config.js} +16 -16
  129. package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
  130. package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
  131. package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
  132. package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
  133. package/src/runtime/advice/{record.ts → record.js} +19 -19
  134. package/src/runtime/advice/{registry.ts → registry.js} +16 -16
  135. package/src/runtime/advice/{strength.ts → strength.js} +7 -7
  136. package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
  137. package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
  138. package/src/runtime/candidate.js +505 -0
  139. package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
  140. package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
  141. package/src/runtime/{claims.ts → claims.js} +44 -35
  142. package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
  143. package/src/runtime/{cli.ts → cli.js} +59 -292
  144. package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
  145. package/src/runtime/context/budget.js +203 -0
  146. package/src/runtime/context/compose.js +1367 -0
  147. package/src/runtime/context/delivery-store.js +102 -0
  148. package/src/runtime/context/mandatory.js +594 -0
  149. package/src/runtime/context/pointer.js +142 -0
  150. package/src/runtime/context-pack.js +1277 -0
  151. package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
  152. package/src/runtime/evidence-fields.js +39 -0
  153. package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
  154. package/src/runtime/failure/{capture.ts → capture.js} +29 -29
  155. package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
  156. package/src/runtime/failure/{cli.ts → cli.js} +30 -30
  157. package/src/runtime/failure/{home.ts → home.js} +6 -6
  158. package/src/runtime/failure/{store.ts → store.js} +32 -32
  159. package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
  160. package/src/runtime/{freshness.ts → freshness.js} +59 -22
  161. package/src/runtime/{health.ts → health.js} +72 -72
  162. package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
  163. package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
  164. package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
  165. package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
  166. package/src/runtime/inventory/{index.ts → index.js} +36 -36
  167. package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
  168. package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
  169. package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
  170. package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
  171. package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
  172. package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
  173. package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
  174. package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
  175. package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
  176. package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
  177. package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
  178. package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
  179. package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
  180. package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
  181. package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
  182. package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
  183. package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
  184. package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
  185. package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
  186. package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
  187. package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
  188. package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
  189. package/src/runtime/{mask.ts → mask.js} +22 -22
  190. package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
  191. package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
  192. package/src/runtime/{model-router.ts → model-router.js} +20 -20
  193. package/src/runtime/{outcome.ts → outcome.js} +27 -27
  194. package/src/runtime/{reach.ts → reach.js} +21 -21
  195. package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
  196. package/src/runtime/{readiness.ts → readiness.js} +113 -43
  197. package/src/runtime/{recovery.ts → recovery.js} +15 -15
  198. package/src/runtime/{research-request.ts → research-request.js} +34 -34
  199. package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
  200. package/src/runtime/retrieval-record.js +223 -0
  201. package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
  202. package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
  203. package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
  204. package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
  205. package/src/runtime/source-version.js +289 -0
  206. package/src/runtime/storage/{cli.ts → cli.js} +9 -9
  207. package/src/runtime/storage/{engine.ts → engine.js} +37 -37
  208. package/src/runtime/storage/{registry.ts → registry.js} +19 -19
  209. package/src/runtime/storage/{roots.ts → roots.js} +8 -8
  210. package/src/runtime/storage/{rules.ts → rules.js} +26 -26
  211. package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
  212. package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
  213. package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
  214. package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
  215. package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
  216. package/src/runtime/{task-state.ts → task-state.js} +86 -21
  217. package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
  218. package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
  219. package/src/runtime/trace/{render.ts → render.js} +7 -7
  220. package/src/runtime/trace/{views.ts → views.js} +40 -40
  221. package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
  222. package/src/runtime/verification/{engine.ts → engine.js} +143 -143
  223. package/src/runtime/verification/{registry.ts → registry.js} +6 -6
  224. package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
  225. package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
  226. package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
  227. package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
  228. package/src/runtime/{worktree.ts → worktree.js} +31 -31
  229. package/src/state/{index.ts → index.js} +26 -26
  230. package/src/state/{lockfile.ts → lockfile.js} +20 -20
  231. package/src/state/{run-lock.ts → run-lock.js} +44 -44
  232. package/core/.antigravity-plugin/hooks.json +0 -39
  233. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  234. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  235. package/core/harnesses/shared/hooks/hooks.json +0 -47
  236. package/src/runtime/context-pack.ts +0 -492
@@ -4,9 +4,10 @@
4
4
 
5
5
  const fs = require('node:fs');
6
6
  const path = require('node:path');
7
- const { parseYamlFile } = require('./capability-router.ts');
8
- const { resolveLocator, describeResolution } = require('./locator-resolve.ts');
9
- const { REPO_ROOT } = require('../helper/repo-root.ts');
7
+ const { parseYamlFile } = require('./capability-router.js');
8
+ const { resolveLocator, describeResolution } = require('./locator-resolve.js');
9
+ const { readPlan, bindEvidence, declaredIntentList } = require('./retrieval-record.js');
10
+ const { REPO_ROOT } = require('../helper/repo-root.js');
10
11
 
11
12
  const READINESS_STATES = new Set([
12
13
  'READY',
@@ -37,7 +38,7 @@ const STAGE_ENTRY_DECISIONS = new Set(['ENTER', 'GATHER_FIRST', 'ASK_USER', 'STO
37
38
  * @param {string} [mode='workflow'] 'workflow' | 'standalone'
38
39
  * @returns {{decision: string, reason: string}}
39
40
  */
40
- function stageEntryFor(state: any, mode = 'workflow') {
41
+ function stageEntryFor(state , mode = 'workflow') {
41
42
  if (!EXECUTION_MODES.has(mode)) {
42
43
  throw new Error(`Unknown execution mode '${mode}'. Valid: ${[...EXECUTION_MODES].join(', ')} — the standalone exemption is declared, never inferred.`);
43
44
  }
@@ -60,11 +61,11 @@ function stageEntryFor(state: any, mode = 'workflow') {
60
61
  }
61
62
 
62
63
  class ReadinessEngine {
63
- declare fsImpl: any;
64
- declare repoRoot: any;
65
- declare projectRoot: any;
66
- declare templatePath: any;
67
- declare templates: any;
64
+
65
+
66
+
67
+
68
+
68
69
  /**
69
70
  * @param {Object} [options]
70
71
  * @param {string} [options.repoRoot] the DoFlow install, where readiness-templates.json lives
@@ -75,7 +76,7 @@ class ReadinessEngine {
75
76
  * @param {Object} [options.templates]
76
77
  * @param {Object} [options.fsImpl]
77
78
  */
78
- constructor(options: any = {}) {
79
+ constructor(options = {}) {
79
80
  this.fsImpl = options.fsImpl || fs;
80
81
  this.repoRoot = options.repoRoot || REPO_ROOT;
81
82
  this.projectRoot = options.projectRoot || process.cwd();
@@ -92,7 +93,7 @@ class ReadinessEngine {
92
93
  try {
93
94
  const data = parseYamlFile(this.templatePath, this.fsImpl);
94
95
  this.templates = data.templates || {};
95
- } catch (error: any) {
96
+ } catch (error ) {
96
97
  throw new Error(`Failed to load readiness templates from '${this.templatePath}': ${error.message}`);
97
98
  }
98
99
  }
@@ -104,7 +105,7 @@ class ReadinessEngine {
104
105
  * @param {Object} taskProfile
105
106
  * @returns {Object}
106
107
  */
107
- suggestAction(reqId: any, reqDef: any, taskProfile: any) {
108
+ suggestAction(reqId , reqDef , taskProfile ) {
108
109
  if (reqId === 'root_cause') {
109
110
  return {
110
111
  intent: 'trace-dependency',
@@ -152,7 +153,7 @@ class ReadinessEngine {
152
153
  * @param {Object} claimsManager
153
154
  * @returns {Object} ReadinessReport
154
155
  */
155
- evaluateReadiness(taskProfile: any, evidenceLedger: any, claimsManager: any) {
156
+ evaluateReadiness(taskProfile , evidenceLedger , claimsManager ) {
156
157
  if (!taskProfile || typeof taskProfile !== 'object') {
157
158
  throw new Error('taskProfile must be an object');
158
159
  }
@@ -206,12 +207,25 @@ class ReadinessEngine {
206
207
 
207
208
  // Inspect claims
208
209
  const taskClaims = claimsManager ? claimsManager.getClaims(taskId) : [];
209
- const conflictedClaims = taskClaims.filter((c: any) => c.status === 'conflicted');
210
+ const conflictedClaims = taskClaims.filter((c ) => c.status === 'conflicted');
210
211
  if (conflictedClaims.length > 0) {
211
212
  hasBlocked = true;
212
213
  }
213
214
 
214
- const taskEvidence = evidenceLedger ? evidenceLedger.queryEvidence({ taskId, status: 'FRESH' }) : [];
215
+ const allTaskEvidence = evidenceLedger ? evidenceLedger.queryEvidence({ taskId }) : [];
216
+ let plan = null;
217
+ let unreadablePlan = null;
218
+ try {
219
+ plan = readPlan(this.projectRoot, taskId);
220
+ } catch (error ) {
221
+ if (error.code !== 'store-unreadable') throw error;
222
+ unreadablePlan = { unreadable: true, reason: error.message };
223
+ }
224
+ const queryBound = Boolean(plan?.planId);
225
+ const binding = queryBound ? bindEvidence(plan, allTaskEvidence) : null;
226
+ const unboundIds = new Set(binding ? binding.unbound.map((item ) => item.id) : []);
227
+ const taskEvidence = allTaskEvidence.filter((item ) => item.freshness?.status === 'FRESH'
228
+ && (!queryBound || !unboundIds.has(item.id)));
215
229
 
216
230
  // An item accepted before FR-004 existed, or one whose target file has since shrunk, can be
217
231
  // FRESH and still point at nothing. *Stale* (the file changed at all) and *unresolvable* (the
@@ -225,8 +239,9 @@ class ReadinessEngine {
225
239
  // in evaluateClaim — it is not a claim that the file never changed. An item that genuinely went
226
240
  // stale and was never superseded still forces NEEDS_EVIDENCE exactly as before.
227
241
  const staleEvidence = (evidenceLedger ? evidenceLedger.getAllEvidence() : [])
228
- .filter((item: any) => item.taskId === taskId && item.freshness?.status === 'STALE' && item.status !== 'superseded')
229
- .map((item: any) => ({ evidenceId: item.id, locator: item.locator, reason: 'file-modified-since-recorded' }));
242
+ .filter((item ) => item.taskId === taskId && item.result !== 'EMPTY'
243
+ && item.freshness?.status === 'STALE' && item.status !== 'superseded')
244
+ .map((item ) => ({ evidenceId: item.id, locator: item.locator, reason: 'file-modified-since-recorded' }));
230
245
 
231
246
  const unresolvableEvidence = [];
232
247
  for (const item of taskEvidence) {
@@ -242,9 +257,30 @@ class ReadinessEngine {
242
257
  }
243
258
  }
244
259
 
245
- for (const [reqId, reqDef] of Object.entries<any>(requirements)) {
260
+ const staleAbsence = [];
261
+ const staleAbsenceByRequirement = new Map ();
262
+ for (const item of allTaskEvidence) {
263
+ if (item.result !== 'EMPTY' || item.status !== 'active'
264
+ || !['STALE', 'UNKNOWN'].includes(item.freshness?.status)
265
+ || (queryBound && unboundIds.has(item.id))) continue;
266
+ for (const requirement of Array.isArray(item.establishes) ? item.establishes : []) {
267
+ const entry = { evidenceId: item.id, requirement, provider: item.source?.provider || null,
268
+ freshness: item.freshness.status, refresh: plan?.providers?.[item.source?.provider]?.refresh || null };
269
+ staleAbsence.push(entry);
270
+ const entries = staleAbsenceByRequirement.get(requirement) || [];
271
+ entries.push({ item, entry });
272
+ staleAbsenceByRequirement.set(requirement, entries);
273
+ }
274
+ }
275
+
276
+ const unboundEvidence = queryBound
277
+ ? { count: binding.unbound.length, ids: binding.unbound.slice(0, 5).map((item ) => item.id) }
278
+ : undefined;
279
+
280
+ for (const [reqId, reqDef] of Object.entries (requirements)) {
246
281
  let isSatisfied = false;
247
282
  let reason = null;
283
+ let reasonCode = null;
248
284
  let matchedEvidenceIds = [];
249
285
 
250
286
  // Check verification plan requirement
@@ -260,9 +296,9 @@ class ReadinessEngine {
260
296
  // not by any supported claim the task happens to hold (review R1 — a supported claim about an
261
297
  // unrelated invariant satisfied the bug template's root-cause requirement).
262
298
  else if (reqDef.requiresClaimStatus) {
263
- let matchingClaims = taskClaims.filter((c: any) => c.status === reqDef.requiresClaimStatus);
299
+ let matchingClaims = taskClaims.filter((c ) => c.status === reqDef.requiresClaimStatus);
264
300
  if (reqDef.requiresClaimRole) {
265
- const withRole = matchingClaims.filter((c: any) => c.role === reqDef.requiresClaimRole);
301
+ const withRole = matchingClaims.filter((c ) => c.role === reqDef.requiresClaimRole);
266
302
  if (withRole.length === 0 && matchingClaims.length > 0) {
267
303
  reason = `${matchingClaims.length} claim(s) have status '${reqDef.requiresClaimStatus}' but none `
268
304
  + `declares role '${reqDef.requiresClaimRole}' — add the claim with --role ${reqDef.requiresClaimRole}, `
@@ -272,7 +308,7 @@ class ReadinessEngine {
272
308
  }
273
309
  if (matchingClaims.length > 0) {
274
310
  isSatisfied = true;
275
- matchedEvidenceIds = matchingClaims.flatMap((c: any) => c.supportingEvidence);
311
+ matchedEvidenceIds = matchingClaims.flatMap((c ) => c.supportingEvidence);
276
312
  } else if (!reason) {
277
313
  reason = `Requires at least one claim with status '${reqDef.requiresClaimStatus}'`
278
314
  + `${reqDef.requiresClaimRole ? ` and role '${reqDef.requiresClaimRole}'` : ''}.`;
@@ -286,21 +322,21 @@ class ReadinessEngine {
286
322
  // (review R1, P1 — reproduced through the public CLI).
287
323
  else if (Array.isArray(reqDef.evidenceKinds) && reqDef.evidenceKinds.length > 0) {
288
324
  const allowedKinds = new Set(reqDef.evidenceKinds);
289
- const kindMatched = taskEvidence.filter((ev: any) => allowedKinds.has(ev.kind));
290
- const extracted = kindMatched.filter((ev: any) => ev.provenance === 'extracted');
291
- const bound = extracted.filter((ev: any) => Array.isArray(ev.establishes) && ev.establishes.includes(reqId));
292
- let qualifying = bound;
293
- if (reqDef.requiresObservation) {
294
- qualifying = qualifying.filter((ev: any) => ev.observation && Number.isInteger(ev.observation.exitCode));
295
- if (reqDef.expectedExit === 'zero') {
296
- qualifying = qualifying.filter((ev: any) => ev.observation.exitCode === 0);
297
- } else if (reqDef.expectedExit === 'nonzero') {
298
- qualifying = qualifying.filter((ev: any) => ev.observation.exitCode !== 0);
299
- }
300
- }
325
+ const rawKindMatched = allTaskEvidence.filter((ev ) => allowedKinds.has(ev.kind));
326
+ const kindMatched = taskEvidence.filter((ev ) => allowedKinds.has(ev.kind));
327
+ const extracted = kindMatched.filter((ev ) => ev.provenance === 'extracted');
328
+ const bound = extracted.filter((ev ) => Array.isArray(ev.establishes) && ev.establishes.includes(reqId));
329
+ const observationOk = (ev ) => !reqDef.requiresObservation || (ev.observation
330
+ && Number.isInteger(ev.observation.exitCode)
331
+ && (reqDef.expectedExit !== 'zero' || ev.observation.exitCode === 0)
332
+ && (reqDef.expectedExit !== 'nonzero' || ev.observation.exitCode !== 0));
333
+ const qualifies = (ev ) => ev.provenance === 'extracted'
334
+ && Array.isArray(ev.establishes) && ev.establishes.includes(reqId)
335
+ && observationOk(ev);
336
+ const qualifying = taskEvidence.filter((ev ) => allowedKinds.has(ev.kind) && qualifies(ev));
301
337
  if (qualifying.length > 0) {
302
338
  isSatisfied = true;
303
- matchedEvidenceIds = qualifying.map((ev: any) => ev.id);
339
+ matchedEvidenceIds = qualifying.map((ev ) => ev.id);
304
340
  } else if (kindMatched.length === 0) {
305
341
  reason = `Missing fresh evidence of kind: ${reqDef.evidenceKinds.join(', ')}.`;
306
342
  } else if (extracted.length === 0) {
@@ -310,8 +346,8 @@ class ReadinessEngine {
310
346
  reason = `${extracted.length} extracted item(s) of the right kind exist but none declares it `
311
347
  + `establishes '${reqId}' — record evidence with establishes: ['${reqId}'] so the gate grades `
312
348
  + 'what the item was actually gathered to prove.';
313
- } else if (reqDef.expectedExit === 'zero' && bound.some((ev: any) => ev.observation && ev.observation.exitCode !== 0)) {
314
- const failed = bound.find((ev: any) => ev.observation && ev.observation.exitCode !== 0);
349
+ } else if (reqDef.expectedExit === 'zero' && bound.some((ev ) => ev.observation && ev.observation.exitCode !== 0)) {
350
+ const failed = bound.find((ev ) => ev.observation && ev.observation.exitCode !== 0);
315
351
  reason = `The bound observation ran '${failed.observation.command}' and exited ${failed.observation.exitCode}; `
316
352
  + 'this requirement needs an exit status of 0. A failed run does not establish a passing baseline.';
317
353
  } else {
@@ -319,6 +355,32 @@ class ReadinessEngine {
319
355
  + `${reqDef.expectedExit ? ` with a${reqDef.expectedExit === 'zero' ? ' passing (0)' : ' non-zero'} exit status` : ''} — `
320
356
  + 'record the execution itself, not a description of one.';
321
357
  }
358
+ if (!isSatisfied && reqDef.required) {
359
+ const staleAbsences = staleAbsenceByRequirement.get(reqId) || [];
360
+ if (staleAbsences.length > 0) {
361
+ const { item, entry } = staleAbsences[0];
362
+ const refresh = entry.refresh || 'none declared';
363
+ reasonCode = 'stale-absence-unverified';
364
+ const prefix = `'${reqId}' rests only on an empty answer from ${entry.provider || 'unknown provider'}, whose index was ${entry.freshness} when it was recorded (${item.id}). An empty answer from that index cannot establish absence. `;
365
+ if (item.freshness?.staleReason === 'tree-changed-since-recorded'
366
+ || item.freshness?.staleReason === 'tree-unverifiable') {
367
+ reason = `${prefix}the working tree changed since it was recorded; re-run the lookup and record the absence again.`;
368
+ } else if (item.freshness?.gitCommit === null) {
369
+ reason = `${prefix}absence cannot be established outside a git work tree; record a positive item.`;
370
+ } else {
371
+ reason = `${prefix}Run the declared refresh command (${refresh}) and record the absence again, or re-run the lookup with a live provider such as native.rg: doflow evidence --task-id ${taskId} --action add --absence --need <intent> --provider native.rg ...`;
372
+ }
373
+ } else if (queryBound) {
374
+ const unboundKind = rawKindMatched.filter((ev ) => unboundIds.has(ev.id));
375
+ const unboundQualifying = unboundKind.filter((ev ) =>
376
+ ev.freshness?.status === 'FRESH' && qualifies(ev));
377
+ if (unboundQualifying.length > 0) {
378
+ const ids = unboundQualifying.slice(0, 5).map((item ) => item.id).join(', ');
379
+ reasonCode = 'unbound-retrieval-evidence';
380
+ reason = `${unboundQualifying.length} item(s) of the right kind are not bound to a declared need of plan ${plan.planId} (${ids}), so they do not count. Re-record them with --need <intent>; declared: ${declaredIntentList(plan)}.`;
381
+ }
382
+ }
383
+ }
322
384
  }
323
385
  // Scope verified / scope clear
324
386
  else if (reqId === 'scope_clear' || reqId === 'scope_verified' || reqId === 'invariants_captured') {
@@ -347,6 +409,7 @@ class ReadinessEngine {
347
409
  required: Boolean(reqDef.required),
348
410
  satisfied: isSatisfied,
349
411
  reason,
412
+ reasonCode,
350
413
  evidenceIds: matchedEvidenceIds,
351
414
  recommendedAction: isSatisfied ? null : this.suggestAction(reqId, reqDef, taskProfile),
352
415
  });
@@ -383,6 +446,10 @@ class ReadinessEngine {
383
446
  + 'Re-record them against the tree as it stands.';
384
447
  }
385
448
 
449
+ if (unreadablePlan) {
450
+ summary += ` Warning: retrieval plan is unreadable and was treated as absent: ${unreadablePlan.reason}`;
451
+ }
452
+
386
453
  return {
387
454
  taskId,
388
455
  taskClass,
@@ -392,13 +459,16 @@ class ReadinessEngine {
392
459
  requirements: requirementResults,
393
460
  claimsSummary: {
394
461
  total: taskClaims.length,
395
- supported: taskClaims.filter((c: any) => c.status === 'supported').length,
396
- hypotheses: taskClaims.filter((c: any) => c.status === 'hypothesis').length,
462
+ supported: taskClaims.filter((c ) => c.status === 'supported').length,
463
+ hypotheses: taskClaims.filter((c ) => c.status === 'hypothesis').length,
397
464
  conflicts: conflictedClaims.length,
398
465
  },
399
466
  evidenceCount: taskEvidence.length,
400
467
  unresolvableEvidence,
401
468
  staleEvidence,
469
+ staleAbsence,
470
+ ...(unreadablePlan ? { retrievalPlan: unreadablePlan } : {}),
471
+ ...(unboundEvidence ? { unboundEvidence } : {}),
402
472
  };
403
473
  }
404
474
  }
@@ -406,18 +476,18 @@ class ReadinessEngine {
406
476
  /** One live snapshot for readiness inspection and orchestration transitions. Reads never persist
407
477
  * derived freshness or claim status; every invocation evaluates the current project again.
408
478
  * `mode` defaults to 'workflow' — fail closed: the standalone exemption must be declared. */
409
- function evaluateTaskReadiness({ taskProfile, repoRoot = REPO_ROOT, projectRoot = process.cwd(), mode = 'workflow' }: any) {
479
+ function evaluateTaskReadiness({ taskProfile, repoRoot = REPO_ROOT, projectRoot = process.cwd(), treeRoot = projectRoot, mode = 'workflow' } ) {
410
480
  // Validated before any ledger work, not just inside stageEntryFor at the end — an invalid mode
411
481
  // used to pay a full freshness/claims evaluation before erroring (review suggestion, 026).
412
482
  if (!EXECUTION_MODES.has(mode)) {
413
483
  throw new Error(`Unknown execution mode '${mode}'. Valid: ${[...EXECUTION_MODES].join(', ')} — the standalone exemption is declared, never inferred.`);
414
484
  }
415
- const { EvidenceLedger } = require('./evidence-ledger.ts');
416
- const { ClaimsManager } = require('./claims.ts');
417
- const { FreshnessValidator } = require('./freshness.ts');
485
+ const { EvidenceLedger } = require('./evidence-ledger.js');
486
+ const { ClaimsManager } = require('./claims.js');
487
+ const { FreshnessValidator } = require('./freshness.js');
418
488
  const ledger = new EvidenceLedger({ repoRoot: projectRoot });
419
489
  ledger.load(taskProfile.taskId);
420
- new FreshnessValidator({ repoRoot: projectRoot }).validateLedgerFreshness(ledger);
490
+ new FreshnessValidator({ repoRoot: projectRoot, treeRoot }).validateLedgerFreshness(ledger);
421
491
  const claims = new ClaimsManager({ evidenceLedger: ledger, repoRoot: projectRoot });
422
492
  claims.load(taskProfile.taskId);
423
493
  claims.evaluateAll();
@@ -20,10 +20,10 @@
20
20
  * names either raised or silently iterated characters.
21
21
  */
22
22
 
23
- const { finishRuntime, usageError } = require('./cli-result.ts');
23
+ const { finishRuntime, usageError } = require('./cli-result.js');
24
24
 
25
25
  /** The eleven classes, in the order the Python declared them. */
26
- const FAILURE_CLASSES: readonly string[] = Object.freeze([
26
+ const FAILURE_CLASSES = Object.freeze([
27
27
  'CONTEXT_MISSING',
28
28
  'WRONG_IMPLEMENTATION',
29
29
  'TEST_FAILURE',
@@ -40,9 +40,9 @@ const FAILURE_CLASSES: readonly string[] = Object.freeze([
40
40
  const DEFAULT_MAX_ITERATIONS = 3;
41
41
 
42
42
  /** Check-name fragments that identify a build-stage failure regardless of the error text. */
43
- const BUILD_CHECK_MARKERS: readonly string[] = Object.freeze(['syntax', 'compile', 'build']);
43
+ const BUILD_CHECK_MARKERS = Object.freeze(['syntax', 'compile', 'build']);
44
44
  /** Check-name fragments that identify a test-stage failure. */
45
- const TEST_CHECK_MARKERS: readonly string[] = Object.freeze(['test', 'unit']);
45
+ const TEST_CHECK_MARKERS = Object.freeze(['test', 'unit']);
46
46
 
47
47
  /**
48
48
  * Per-class recovery strategy — **one entry per class, all eleven** (plan task C.2).
@@ -68,7 +68,7 @@ const TEST_CHECK_MARKERS: readonly string[] = Object.freeze(['test', 'unit']);
68
68
  * already been spent (its `current_iteration >= 2`). `terminal` is new and means the class is not
69
69
  * retryable at any iteration.
70
70
  */
71
- const STRATEGY_MAP: Readonly<Record<string, any>> = Object.freeze({
71
+ const STRATEGY_MAP = Object.freeze({
72
72
  CONTEXT_MISSING: { action: 'RETRIEVE_MORE_CONTEXT', targetRole: 'SCOUT', switchAtIteration: null },
73
73
  WRONG_IMPLEMENTATION: { action: 'REVISE_IMPLEMENTATION', targetRole: 'IMPLEMENTER', switchAtIteration: 2 },
74
74
  TEST_FAILURE: { action: 'TARGETED_DEBUGGER_FIX', targetRole: 'DEBUGGER', switchAtIteration: 2 },
@@ -86,7 +86,7 @@ const STRATEGY_MAP: Readonly<Record<string, any>> = Object.freeze({
86
86
  });
87
87
 
88
88
  /** Reachable only for a class string outside the eleven — see the note on STRATEGY_MAP. */
89
- const GENERIC_STRATEGY: Readonly<Record<string, any>> = Object.freeze({
89
+ const GENERIC_STRATEGY = Object.freeze({
90
90
  action: 'GENERIC_DEBUGGER_FIX',
91
91
  targetRole: 'DEBUGGER',
92
92
  switchAtIteration: 2,
@@ -96,23 +96,23 @@ const GENERIC_STRATEGY: Readonly<Record<string, any>> = Object.freeze({
96
96
  * @param {*} value
97
97
  * @returns {Array<string>} only the string members, lowercased
98
98
  */
99
- function normalizeCheckNames(value: any): string[] {
99
+ function normalizeCheckNames(value ) {
100
100
  // Defect fix (2a): the Python accepted `failed_checks` unvalidated. Handed a plain string it
101
101
  // iterated characters, so every single-letter "check" was tested for "syntax" and none matched —
102
102
  // a build failure silently classified as WRONG_IMPLEMENTATION.
103
103
  if (!Array.isArray(value)) return [];
104
- return value.filter((v: any) => typeof v === 'string').map((v: string) => v.toLowerCase());
104
+ return value.filter((v ) => typeof v === 'string').map((v ) => v.toLowerCase());
105
105
  }
106
106
 
107
107
  class RecoveryManager {
108
- maxIterations: number;
108
+ maxIterations ;
109
109
 
110
110
  /**
111
111
  * @param {Object|number} [options] a number is accepted for parity with the Python's positional
112
112
  * `max_iterations` argument
113
113
  * @param {number} [options.maxIterations=3]
114
114
  */
115
- constructor(options: any = {}) {
115
+ constructor(options = {}) {
116
116
  const opts = typeof options === 'number' ? { maxIterations: options } : options;
117
117
  this.maxIterations = opts.maxIterations || DEFAULT_MAX_ITERATIONS;
118
118
  }
@@ -126,13 +126,13 @@ class RecoveryManager {
126
126
  * @param {Array<string>} [failedChecks=[]]
127
127
  * @returns {string} one of FAILURE_CLASSES
128
128
  */
129
- classifyFailure(errorMessage: any, failedChecks: any = []): string {
129
+ classifyFailure(errorMessage , failedChecks = []) {
130
130
  const checks = normalizeCheckNames(failedChecks);
131
131
  // Defect fix (2b): `None.lower()` raised. A classifier handed no message should return
132
132
  // UNKNOWN_FAILURE, not abort the recovery path that exists to handle failures.
133
133
  const msg = typeof errorMessage === 'string' ? errorMessage.toLowerCase() : '';
134
134
 
135
- const anyCheck = (markers: readonly string[]) => checks.some((chk) => markers.some((m) => chk.includes(m)));
135
+ const anyCheck = (markers ) => checks.some((chk) => markers.some((m) => chk.includes(m)));
136
136
 
137
137
  if (anyCheck(BUILD_CHECK_MARKERS) || msg.includes('build error')) {
138
138
  return 'BUILD_FAILURE';
@@ -174,7 +174,7 @@ class RecoveryManager {
174
174
  * @param {string} [lastAgent='codex']
175
175
  * @returns {{ iteration: number, failureClass: string, action: string, targetRole: string|null, agent: string, canRetry: boolean, reason?: string }}
176
176
  */
177
- planRecovery(failureClass: string, currentIteration: number, lastAgent: string = 'codex'): { iteration: number; failureClass: string; action: string; targetRole: string | null; agent: string; canRetry: boolean; reason?: string } {
177
+ planRecovery(failureClass , currentIteration , lastAgent = 'codex') {
178
178
  // A negative or non-numeric iteration is caller nonsense; clamping keeps it from reading as
179
179
  // "before the first attempt" and skipping the agent switch that iteration 0 is supposed to get.
180
180
  const iteration = Number.isFinite(currentIteration) ? Math.max(0, Math.trunc(currentIteration)) : 0;
@@ -248,7 +248,7 @@ class RecoveryManager {
248
248
  * @param {boolean} [options.json=false]
249
249
  * @returns {number} exit code
250
250
  */
251
- function handleRecoverCommand({ errorMessage, failedChecks = [], iteration = 0, agent, json = false }: any = {}): number {
251
+ function handleRecoverCommand({ errorMessage, failedChecks = [], iteration = 0, agent, json = false } = {}) {
252
252
  // Handed nothing, the classifier answers UNKNOWN_FAILURE — a confident-looking verdict about a
253
253
  // failure it never saw. Refuse instead.
254
254
  if ((typeof errorMessage !== 'string' || errorMessage.trim() === '') && failedChecks.length === 0) {
@@ -258,7 +258,7 @@ function handleRecoverCommand({ errorMessage, failedChecks = [], iteration = 0,
258
258
  const failureClass = manager.classifyFailure(errorMessage, failedChecks);
259
259
  const plan = manager.planRecovery(failureClass, iteration, agent || undefined);
260
260
 
261
- if (json) console.log(JSON.stringify({ failureClass, failedChecks, ...(plan as any) }, null, 2));
261
+ if (json) console.log(JSON.stringify({ failureClass, failedChecks, ...(plan ) }, null, 2));
262
262
  else {
263
263
  console.log(`\nDoFlow Recovery [${plan.failureClass}]:`);
264
264
  console.log('═'.repeat(78));
@@ -5,24 +5,24 @@
5
5
  const fs = require('node:fs');
6
6
  const path = require('node:path');
7
7
  const crypto = require('node:crypto');
8
- const { acquireLock, updateTaskState, readTaskState } = require('./task-state.ts');
9
- const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.ts');
10
- const { ClaimsManager } = require('./claims.ts');
11
- const { taskStoreDir } = require('./task-scope.ts');
12
- const { FreshnessValidator } = require('./freshness.ts');
13
- const { resolveLocator } = require('./locator-resolve.ts');
14
- const { finishRuntime, usageError } = require('./cli-result.ts');
8
+ const { acquireLock, updateTaskState, readTaskState } = require('./task-state.js');
9
+ const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.js');
10
+ const { ClaimsManager } = require('./claims.js');
11
+ const { taskStoreDir } = require('./task-scope.js');
12
+ const { FreshnessValidator } = require('./freshness.js');
13
+ const { resolveLocator } = require('./locator-resolve.js');
14
+ const { finishRuntime, usageError } = require('./cli-result.js');
15
15
 
16
16
  const STATES = new Set(['OPEN', 'ANSWERED', 'UNRESOLVED']);
17
17
  const REASONS = new Set(['detected-gap', 'user-request']);
18
- const safeId = (value: any, label: any) => {
18
+ const safeId = (value , label ) => {
19
19
  if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
20
20
  throw new Error(`${label} must be a safe non-empty identifier`);
21
21
  }
22
22
  return value;
23
23
  };
24
24
 
25
- function hasLocatedSource(item: any, projectRoot: any) {
25
+ function hasLocatedSource(item , projectRoot ) {
26
26
  const locator = item.locator || {};
27
27
  if (locator.uri !== undefined) {
28
28
  if (typeof locator.uri !== 'string') return false;
@@ -36,11 +36,11 @@ function hasLocatedSource(item: any, projectRoot: any) {
36
36
  }
37
37
 
38
38
  class ResearchRequestStore {
39
- declare projectRoot: any;
40
- declare fsImpl: any;
41
- declare slug: any;
42
- declare stateDir: any;
43
- constructor({ projectRoot = process.cwd(), fsImpl = fs, slug = null }: any = {}) {
39
+
40
+
41
+
42
+
43
+ constructor({ projectRoot = process.cwd(), fsImpl = fs, slug = null } = {}) {
44
44
  this.projectRoot = projectRoot;
45
45
  this.fsImpl = fsImpl;
46
46
  this.slug = slug;
@@ -48,29 +48,29 @@ class ResearchRequestStore {
48
48
  }
49
49
 
50
50
  /** The feature's namespace directory when the task has one (task-scope.js), else the flat store. */
51
- file(taskId: any) {
51
+ file(taskId ) {
52
52
  const dir = taskStoreDir({ projectRoot: this.projectRoot, store: 'research', taskId: assertSafeTaskId(taskId), slug: this.slug });
53
53
  return path.join(dir, `${taskId}.json`);
54
54
  }
55
55
 
56
- runFile(taskId: any) {
56
+ runFile(taskId ) {
57
57
  return path.join(this.projectRoot, '.doflow', 'state', 'orchestration', `${assertSafeTaskId(taskId)}.json`);
58
58
  }
59
59
 
60
- read(taskId: any) {
60
+ read(taskId ) {
61
61
  const data = readTaskState(this.fsImpl, this.file(taskId));
62
62
  if (!data) return { taskId, revision: 0, requests: [] };
63
63
  if (data.taskId !== taskId || !Array.isArray(data.requests)
64
- || data.requests.some((r: any) => !STATES.has(r.status))) {
64
+ || data.requests.some((r ) => !STATES.has(r.status))) {
65
65
  throw new Error(`Invalid research state for task '${taskId}'`);
66
66
  }
67
67
  return data;
68
68
  }
69
69
 
70
70
  // Re-read inside updateTaskState's lock: overlapping writers cannot lose a request.
71
- change(taskId: any, update: any) {
71
+ change(taskId , update ) {
72
72
  let result;
73
- updateTaskState({ fsImpl: this.fsImpl, file: this.file(taskId), build: (disk: any) => {
73
+ updateTaskState({ fsImpl: this.fsImpl, file: this.file(taskId), build: (disk ) => {
74
74
  if (disk && (disk.taskId !== taskId || !Array.isArray(disk.requests))) {
75
75
  throw new Error(`Invalid research state for task '${taskId}'`);
76
76
  }
@@ -81,7 +81,7 @@ class ResearchRequestStore {
81
81
  return result;
82
82
  }
83
83
 
84
- stageFor(taskId: any, stageId: any) {
84
+ stageFor(taskId , stageId ) {
85
85
  const run = readTaskState(this.fsImpl, this.runFile(taskId));
86
86
  if (!run || run.taskClass !== 'feature' || run.state !== 'RUNNING') {
87
87
  throw new Error(`Task '${taskId}' has no active feature stage`);
@@ -92,7 +92,7 @@ class ResearchRequestStore {
92
92
  }
93
93
  }
94
94
 
95
- open({ taskId, stageId, question, reason, blocking }: any = {}) {
95
+ open({ taskId, stageId, question, reason, blocking } = {}) {
96
96
  safeId(taskId, 'taskId'); safeId(stageId, 'stageId');
97
97
  if (typeof question !== 'string' || !question.trim()) throw new Error('question is required');
98
98
  if (!REASONS.has(reason)) throw new Error(`reason must be one of: ${[...REASONS].join(', ')}`);
@@ -104,8 +104,8 @@ class ResearchRequestStore {
104
104
  const release = acquireLock(this.fsImpl, runFile);
105
105
  try {
106
106
  this.stageFor(taskId, stageId);
107
- return this.change(taskId, (requests: any) => {
108
- const existing = requests.find((r: any) => r.stageId === stageId && r.question === question.trim() && r.status === 'OPEN');
107
+ return this.change(taskId, (requests ) => {
108
+ const existing = requests.find((r ) => r.stageId === stageId && r.question === question.trim() && r.status === 'OPEN');
109
109
  if (existing) {
110
110
  if ((blocking ?? reason === 'detected-gap') && !existing.blocking) {
111
111
  existing.blocking = true;
@@ -124,13 +124,13 @@ class ResearchRequestStore {
124
124
  } finally { release(); }
125
125
  }
126
126
 
127
- list(taskId: any) {
127
+ list(taskId ) {
128
128
  safeId(taskId, 'taskId');
129
129
  return this.read(taskId).requests;
130
130
  }
131
131
 
132
132
  // Validate references before taking the write lock; only the status transition itself mutates state.
133
- resolve({ taskId, requestId, outcome, claimId, evidenceIds = [], gap }: any = {}) {
133
+ resolve({ taskId, requestId, outcome, claimId, evidenceIds = [], gap } = {}) {
134
134
  safeId(taskId, 'taskId'); safeId(requestId, 'requestId');
135
135
  if (!['answered', 'unresolved'].includes(outcome)) throw new Error('outcome must be answered or unresolved');
136
136
  if (!Array.isArray(evidenceIds)) throw new Error('evidenceIds must be an array');
@@ -159,8 +159,8 @@ class ResearchRequestStore {
159
159
  }
160
160
  }
161
161
  }
162
- return this.change(taskId, (requests: any) => {
163
- const request = requests.find((r: any) => r.id === requestId);
162
+ return this.change(taskId, (requests ) => {
163
+ const request = requests.find((r ) => r.id === requestId);
164
164
  if (!request) throw new Error(`Unknown research request '${requestId}' for task '${taskId}'`);
165
165
  if (request.status !== 'OPEN') throw new Error(`Request '${requestId}' is ${request.status}, not OPEN`);
166
166
  Object.assign(request, { status: outcome === 'answered' ? 'ANSWERED' : 'UNRESOLVED',
@@ -170,20 +170,20 @@ class ResearchRequestStore {
170
170
  });
171
171
  }
172
172
 
173
- blockingGap(taskId: any, stageId: any) {
174
- const requests = this.list(taskId).filter((r: any) => r.stageId === stageId && r.blocking);
173
+ blockingGap(taskId , stageId ) {
174
+ const requests = this.list(taskId).filter((r ) => r.stageId === stageId && r.blocking);
175
175
  for (const request of requests) {
176
176
  if (request.status !== 'ANSWERED') return request;
177
177
  try {
178
178
  this.validateAnswer(taskId, request);
179
- } catch (error: any) {
179
+ } catch (error ) {
180
180
  return { ...request, failureReason: error.message };
181
181
  }
182
182
  }
183
183
  return null;
184
184
  }
185
185
 
186
- validateAnswer(taskId: any, request: any) {
186
+ validateAnswer(taskId , request ) {
187
187
  const ledger = new EvidenceLedger({ repoRoot: this.projectRoot, slug: this.slug });
188
188
  ledger.load(taskId);
189
189
  new FreshnessValidator({ repoRoot: this.projectRoot }).validateLedgerFreshness(ledger);
@@ -210,7 +210,7 @@ class ResearchRequestStore {
210
210
  }
211
211
 
212
212
  function handleResearchRequestCommand({ action = 'list', taskId, stageId, question, reason,
213
- blocking, requestId, outcome, claimId, evidenceIds, gap, json = false, projectRoot, slug }: any = {}) {
213
+ blocking, requestId, outcome, claimId, evidenceIds, gap, json = false, projectRoot, slug } = {}) {
214
214
  try {
215
215
  const store = new ResearchRequestStore({ projectRoot, slug });
216
216
  let result;
@@ -223,7 +223,7 @@ function handleResearchRequestCommand({ action = 'list', taskId, stageId, questi
223
223
  if (json) console.log(JSON.stringify(result, null, 2));
224
224
  else console.log(JSON.stringify(result));
225
225
  return finishRuntime(0);
226
- } catch (error: any) {
226
+ } catch (error ) {
227
227
  return usageError('research-request', error.message, json, error);
228
228
  }
229
229
  }