@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
@@ -38,18 +38,17 @@
38
38
  * emits are counts of records.
39
39
  */
40
40
 
41
- const fs = require('node:fs');
42
- const path = require('node:path');
43
- const { CapabilityRouter } = require('./capability-router.ts');
44
- const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.ts');
45
- const { taskStoreDir } = require('./task-scope.ts');
46
- const { probeFreshness } = require('./health.ts');
41
+ const { CapabilityRouter } = require('./capability-router.js');
42
+ const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.js');
43
+ const { FreshnessValidator } = require('./freshness.js');
44
+ const { probeFreshness } = require('./health.js');
45
+ const { planPath, readPlan, updatePlan, moveCorruptPlanAside, mintPlanId, activeAttemptRef, resolveNeed, bindEvidence, deriveOutcome } = require('./retrieval-record.js');
47
46
  // One definition of the score refusal, applied at a second boundary. See the note on
48
- // EVIDENCE_SCORE_FIELDS in cli.js: relevance is a property of a search, not of a fact, and a plan
47
+ // EVIDENCE_SCORE_FIELDS in src/runtime/evidence-fields.js: relevance is a property of a search, not of a fact, and a plan
49
48
  // that recorded one would hand the next worker a confidence dressed as a measurement.
50
- const { assertNoScoreFields } = require('./cli.ts');
51
- const { finishRuntime, usageError } = require('./cli-result.ts');
52
- const { REPO_ROOT } = require('../helper/repo-root.ts');
49
+ const { assertNoScoreFields } = require('./evidence-fields.js');
50
+ const { finishRuntime, usageError } = require('./cli-result.js');
51
+ const { REPO_ROOT } = require('../helper/repo-root.js');
53
52
 
54
53
  // ── the closed result vocabulary (design §4) ─────────────────────────────────────────────────
55
54
 
@@ -73,6 +72,18 @@ const RESULTS = Object.freeze([RETRIEVED, EMPTY, UNREACHED, UNVERIFIED]);
73
72
  const NOT_APPLICABLE = 'NOT_APPLICABLE';
74
73
  const FRESHNESS_STATES = Object.freeze(['FRESH', 'STALE', 'UNKNOWN', NOT_APPLICABLE]);
75
74
 
75
+ // ── attempt reason codes (IC-013) ─────────────────────────────────────────────────────────────
76
+
77
+ const ATTEMPT_REASON_CODES = Object.freeze([
78
+ 'provider-unavailable',
79
+ 'policy-bypass',
80
+ 'provider-error',
81
+ 'timeout',
82
+ 'empty-answer',
83
+ 'index-stale',
84
+ 'index-unverifiable',
85
+ ]);
86
+
76
87
  /**
77
88
  * Design §4's mapping table, as data. It maps only the *empty* answer: a provider that returned
78
89
  * something is RETRIEVED whatever the age of its index, because the thing it returned is a
@@ -84,19 +95,26 @@ const FRESHNESS_STATES = Object.freeze(['FRESH', 'STALE', 'UNKNOWN', NOT_APPLICA
84
95
  * this module exists to prevent — so staleness travels as a marker beside `EMPTY`, and the
85
96
  * provider's `refresh` command travels with it so the remedy is one step away.
86
97
  */
87
- const EMPTY_ANSWER_BY_FRESHNESS: Record<string, any> = Object.freeze({
98
+ const EMPTY_ANSWER_BY_FRESHNESS = Object.freeze({
88
99
  UNKNOWN: UNVERIFIED,
89
100
  STALE: EMPTY,
90
101
  FRESH: EMPTY,
91
102
  [NOT_APPLICABLE]: EMPTY,
92
103
  });
93
104
 
105
+ function requirePlanForUpdate(disk , taskId ) {
106
+ if (disk === null) {
107
+ throw new Error(`the retrieval plan for task ${taskId} was removed during the update; re-declare it`);
108
+ }
109
+ return disk;
110
+ }
111
+
94
112
  /**
95
113
  * What an empty answer becomes, given the freshness of the index behind it.
96
114
  * @param {string} freshnessState one of FRESHNESS_STATES
97
115
  * @returns {string} one of RESULTS
98
116
  */
99
- function resultForEmptyAnswer(freshnessState: any) {
117
+ function resultForEmptyAnswer(freshnessState ) {
100
118
  const mapped = EMPTY_ANSWER_BY_FRESHNESS[freshnessState];
101
119
  if (!mapped) {
102
120
  // Fail closed rather than pick a default: an unrecognised freshness token means the record was
@@ -107,50 +125,6 @@ function resultForEmptyAnswer(freshnessState: any) {
107
125
  return mapped;
108
126
  }
109
127
 
110
- // ── the record ────────────────────────────────────────────────────────────────────────────────
111
-
112
- /**
113
- * Where a task's plan lives, under the invoking project's state root.
114
- *
115
- * The task id is validated by the evidence ledger's own rule, unchanged: it becomes a filename, so
116
- * it must never be able to name a path.
117
- * @param {string} projectRoot
118
- * @param {string} taskId
119
- * @param {string|null} [slug] the feature the task belongs to, when not the branch's (task-scope.js)
120
- * @returns {string}
121
- */
122
- function planPath(projectRoot: any, taskId: any, slug: any = null) {
123
- const dir = taskStoreDir({ projectRoot, store: 'retrieval', taskId: assertSafeTaskId(taskId), slug });
124
- return path.join(dir, `${taskId}.json`);
125
- }
126
-
127
- /**
128
- * @param {string} projectRoot
129
- * @param {string} taskId
130
- * @returns {Object|null} the record, or null when no plan was declared
131
- */
132
- function readPlan(projectRoot: any, taskId: any, slug: any = null) {
133
- const file = planPath(projectRoot, taskId, slug);
134
- if (!fs.existsSync(file)) return null;
135
- try {
136
- return JSON.parse(fs.readFileSync(file, 'utf8'));
137
- } catch (error: any) {
138
- throw new Error(`the retrieval plan at '${file}' is not readable: ${error.message}`);
139
- }
140
- }
141
-
142
- /**
143
- * @param {string} projectRoot
144
- * @param {string} taskId
145
- * @param {Object} record
146
- * @returns {string} the file written
147
- */
148
- function writePlan(projectRoot: any, taskId: any, record: any, slug: any = null) {
149
- const file = planPath(projectRoot, taskId, slug);
150
- fs.mkdirSync(path.dirname(file), { recursive: true });
151
- fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
152
- return file;
153
- }
154
128
 
155
129
  /**
156
130
  * The `sourceMode` each provider id declares, as a lookup the plan builds once per declare.
@@ -161,16 +135,16 @@ function writePlan(projectRoot: any, taskId: any, record: any, slug: any = null)
161
135
  * @param {Object} capabilities the router's `capabilities` map
162
136
  * @returns {(providerId: string) => string|null} the declared mode, or null for an unknown id
163
137
  */
164
- function sourceModeLookup(capabilities: any) {
138
+ function sourceModeLookup(capabilities ) {
165
139
  const modes = new Map();
166
- for (const capability of Object.values<any>(capabilities || {})) {
140
+ for (const capability of Object.values (capabilities || {})) {
167
141
  for (const provider of (capability && capability.providers) || []) {
168
142
  if (!provider || !provider.id) continue;
169
143
  if (modes.get(provider.id) === 'index') continue;
170
144
  modes.set(provider.id, provider.sourceMode || null);
171
145
  }
172
146
  }
173
- return (providerId: any) => (modes.has(providerId) ? modes.get(providerId) : null);
147
+ return (providerId ) => (modes.has(providerId) ? modes.get(providerId) : null);
174
148
  }
175
149
 
176
150
  /**
@@ -186,8 +160,8 @@ function sourceModeLookup(capabilities: any) {
186
160
  * absent, every provider without an artifact layout is treated as unknown (fail closed)
187
161
  * @returns {{state: string, artifact: string|null, refresh: string|null, label: string|null, reason: string|null}}
188
162
  */
189
- function freshnessEntry(providerId: any, projectRoot: any, sourceModeOf: any = () => null) {
190
- const probe = probeFreshness(providerId, { root: projectRoot });
163
+ function freshnessEntry(providerId , sourceRoot , sourceModeOf = () => null) {
164
+ const probe = probeFreshness(providerId, { root: sourceRoot });
191
165
  if (!probe) {
192
166
  // `probeFreshness` returns null for a provider it has no artifact layout for. What that means
193
167
  // depends on how the provider answers: a `live` provider reads current files or commands, so
@@ -230,9 +204,9 @@ function freshnessEntry(providerId: any, projectRoot: any, sourceModeOf: any = (
230
204
  * @param {Object} need
231
205
  * @returns {string} one of FRESHNESS_STATES
232
206
  */
233
- function freshnessOf(record: any, need: any) {
207
+ function freshnessOf(record , need ) {
234
208
  if (!need.provider) return NOT_APPLICABLE;
235
- const entry: any = record.providers && record.providers[need.provider];
209
+ const entry = record.providers && record.providers[need.provider];
236
210
  return entry && entry.state ? entry.state : NOT_APPLICABLE;
237
211
  }
238
212
 
@@ -248,14 +222,14 @@ function freshnessOf(record: any, need: any) {
248
222
  * @param {Array<Object>} items
249
223
  * @returns {Array<string>} evidence ids
250
224
  */
251
- function evidenceIdsFor(need: any, items: any) {
225
+ function evidenceIdsFor(need , items ) {
252
226
  return items
253
- .filter((item: any) => {
227
+ .filter((item ) => {
254
228
  const source = item.source || {};
255
229
  if (need.provider && source.provider === need.provider) return true;
256
230
  return Boolean(need.capability) && source.capability === need.capability;
257
231
  })
258
- .map((item: any) => item.id);
232
+ .map((item ) => item.id);
259
233
  }
260
234
 
261
235
  // ── actions ───────────────────────────────────────────────────────────────────────────────────
@@ -274,13 +248,13 @@ function evidenceIdsFor(need: any, items: any) {
274
248
  *
275
249
  * @returns {number} exit code
276
250
  */
277
- function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }: any) {
278
- const needs = [];
251
+ function declarePlan({ router, projectRoot, sourceRoot, taskId, stage, intents, json, slug } ) {
252
+ const needs = [];
279
253
  for (const intent of intents) {
280
254
  let resolution;
281
255
  try {
282
256
  resolution = router.resolveIntent(intent);
283
- } catch (error: any) {
257
+ } catch (error ) {
284
258
  // A misspelled intent is a caller error, not an unresolvable need. Recording it as
285
259
  // declared-unresolvable would file a typo as a machine limitation and hide it until report.
286
260
  return usageError('retrieval-plan',
@@ -299,22 +273,39 @@ function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
299
273
 
300
274
  // R8, in one loop: the probe set is the set of DISTINCT providers, so cost is bounded by
301
275
  // provider count and not by need count. Six needs across two providers pay two scans.
302
- const providers: Record<string, any> = {};
303
- const sourceModeOf: any = sourceModeLookup(router.capabilities);
276
+ const providers = {};
277
+ const sourceModeOf = sourceModeLookup(router.capabilities);
304
278
  for (const need of needs) {
305
279
  if (!need.provider || providers[need.provider]) continue;
306
- providers[need.provider] = freshnessEntry(need.provider, projectRoot, sourceModeOf);
280
+ providers[need.provider] = freshnessEntry(need.provider, sourceRoot, sourceModeOf);
307
281
  }
308
282
 
309
- const record = {
283
+ const planId = mintPlanId();
284
+ const attemptRef = activeAttemptRef(projectRoot, taskId, slug);
285
+ for (const need of needs) {
286
+ need.queryRef = `${planId}#${need.intent}`;
287
+ }
288
+
289
+ const record = {
310
290
  version: 1,
291
+ planId,
292
+ binding: 'query',
311
293
  taskId,
312
294
  stage: stage || null,
313
295
  declaredAt: new Date().toISOString(),
296
+ attemptRef,
314
297
  providers,
315
298
  needs,
316
299
  };
317
- const file = writePlan(projectRoot, taskId, record, slug);
300
+ let file ;
301
+ try {
302
+ file = updatePlan(projectRoot, taskId, slug, () => record, undefined, () => {
303
+ const corruptFile = moveCorruptPlanAside(projectRoot, taskId, slug);
304
+ if (corruptFile) console.log(`Moved unreadable retrieval plan to ${corruptFile}; declaring a new plan.`);
305
+ });
306
+ } catch (error ) {
307
+ return usageError('retrieval-plan', error.message, json, error);
308
+ }
318
309
 
319
310
  const unresolvable = needs.filter((need) => !need.provider).map((need) => need.intent);
320
311
  if (json) {
@@ -326,12 +317,13 @@ function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
326
317
  console.log('═'.repeat(78));
327
318
  console.log(`Stage: ${record.stage || 'not stated'}`);
328
319
  console.log('─'.repeat(78));
329
- console.log('Intent'.padEnd(26) + 'Provider'.padEnd(22) + 'Index');
320
+ console.log('Intent'.padEnd(26) + 'Provider'.padEnd(22) + 'Index'.padEnd(16) + 'QueryRef');
330
321
  for (const need of needs) {
331
322
  console.log(
332
323
  need.intent.padEnd(26)
333
324
  + (need.provider || 'none — unresolvable').padEnd(22)
334
- + freshnessOf(record, need),
325
+ + freshnessOf(record, need).padEnd(16)
326
+ + (need.queryRef || ''),
335
327
  );
336
328
  }
337
329
  printProviderIndexes(record);
@@ -367,11 +359,11 @@ function declarePlan({ router, projectRoot, taskId, stage, intents, json, slug }
367
359
  *
368
360
  * @returns {number} exit code
369
361
  */
370
- function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
371
- let record;
362
+ function reportPlan({ projectRoot, sourceRoot, taskId, reached, json, slug, repoRoot } ) {
363
+ let record ;
372
364
  try {
373
365
  record = readPlan(projectRoot, taskId, slug);
374
- } catch (error: any) {
366
+ } catch (error ) {
375
367
  return usageError('retrieval-plan', error.message, json, error);
376
368
  }
377
369
  if (!record) {
@@ -381,7 +373,7 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
381
373
  + 'plan would state that nothing was missed, which is not something an absent plan can establish', json);
382
374
  }
383
375
 
384
- const declared = new Set(record.needs.map((need: any) => need.intent));
376
+ const declared = new Set(record.needs.map((need ) => need.intent));
385
377
  const undeclared = [...reached].filter((intent) => !declared.has(intent)).sort();
386
378
  if (undeclared.length > 0) {
387
379
  return usageError('retrieval-plan',
@@ -392,25 +384,129 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
392
384
  const ledger = new EvidenceLedger({ repoRoot: projectRoot, slug });
393
385
  try {
394
386
  ledger.load(taskId);
395
- } catch (error: any) {
387
+ } catch (error ) {
396
388
  return usageError('retrieval-plan', error.message, json, error);
397
389
  }
390
+ new FreshnessValidator({ repoRoot: projectRoot, treeRoot: sourceRoot }).validateLedgerFreshness(ledger);
398
391
  const items = ledger.queryEvidence({ taskId });
399
392
 
400
- for (const need of record.needs) {
401
- need.evidenceIds = evidenceIdsFor(need, items);
402
- if (need.evidenceIds.length > 0) need.result = RETRIEVED;
403
- else if (reached.has(need.intent)) need.result = resultForEmptyAnswer(freshnessOf(record, need));
404
- else need.result = UNREACHED;
393
+ const isQueryBound = Boolean(record.planId);
394
+ let binding = isQueryBound ? bindEvidence(record, items) : null;
395
+ let reportSourceModeOf = null;
396
+ const probeByProvider = new Map ();
397
+ if (record.planId) {
398
+ const initialBinding = bindEvidence(record, items);
399
+ const missingProviders = new Set ();
400
+ for (const need of record.needs) {
401
+ for (const item of initialBinding.byNeed.get(need.intent) || []) {
402
+ if (item.result !== 'EMPTY' && !record.providers?.[item.source?.provider]) {
403
+ if (item.source?.provider) missingProviders.add(item.source.provider);
404
+ }
405
+ }
406
+ }
407
+ if (missingProviders.size > 0) {
408
+ const reportRouter = new CapabilityRouter({ repoRoot: repoRoot || REPO_ROOT });
409
+ reportSourceModeOf = sourceModeLookup(reportRouter.capabilities);
410
+ for (const providerId of missingProviders) {
411
+ probeByProvider.set(providerId, module.exports._probe(providerId, sourceRoot, reportSourceModeOf));
412
+ }
413
+ }
405
414
  }
406
415
 
407
- record.reportedAt = new Date().toISOString();
408
- const file = writePlan(projectRoot, taskId, record, slug);
416
+ let file ;
417
+ try {
418
+ file = updatePlan(projectRoot, taskId, slug, (disk ) => {
419
+ const current = requirePlanForUpdate(disk, taskId);
420
+ if (current.planId) {
421
+ const currentBinding = bindEvidence(current, items);
422
+ binding = currentBinding;
423
+ for (const need of current.needs) {
424
+ const bound = currentBinding.byNeed.get(need.intent) || [];
425
+ if (!Array.isArray(need.attempts)) need.attempts = [];
426
+ for (const attempt of need.attempts) {
427
+ if (attempt.recordedBy === 'derived') attempt.evidenceIds = [];
428
+ }
429
+ const groups = new Map ();
430
+ for (const item of bound) {
431
+ const absent = item.result === 'EMPTY';
432
+ const status = absent
433
+ ? (item.freshness?.status === 'UNKNOWN' ? UNVERIFIED : EMPTY)
434
+ : RETRIEVED;
435
+ const providerKey = item.source?.provider ?? '__no_provider__';
436
+ const freshness = item.freshness?.status || 'UNKNOWN';
437
+ const key = `${providerKey}\u0000${status}\u0000${freshness}`;
438
+ if (!groups.has(key)) groups.set(key, []);
439
+ const group = groups.get(key);
440
+ if (group) group.push(item);
441
+ }
442
+ const countedAttempts = need.attempts.filter((attempt ) =>
443
+ !(attempt.recordedBy === 'derived' && attempt.basis !== 'report-need'
444
+ && (!Array.isArray(attempt.evidenceIds) || attempt.evidenceIds.length === 0)));
445
+ if (reached.has(need.intent) && countedAttempts.length === 0 && bound.length === 0) {
446
+ const provider = need.provider;
447
+ const indexState = provider ? (current.providers?.[provider]?.state || 'UNKNOWN') : NOT_APPLICABLE;
448
+ groups.set(`${provider ?? '__no_provider__'}\u0000${resultForEmptyAnswer(indexState)}\u0000${indexState}`, []);
449
+ }
450
+ for (const [key, group] of groups) {
451
+ const [providerKey, status, freshness] = key.split('\u0000');
452
+ const provider = providerKey === '__no_provider__' ? null : providerKey;
453
+ let attempt = need.attempts.find((entry ) => entry.recordedBy === 'derived'
454
+ && entry.provider === provider && entry.status === status && entry.indexState === freshness);
455
+ if (attempt) {
456
+ attempt.evidenceIds = group.map((item ) => item.id);
457
+ continue;
458
+ }
459
+ if (status === RETRIEVED && provider && !current.providers?.[provider] && probeByProvider.has(provider)) {
460
+ current.providers ||= {};
461
+ current.providers[provider] = probeByProvider.get(provider);
462
+ }
463
+ const providerState = provider ? current.providers?.[provider]?.state : null;
464
+ const indexState = status === RETRIEVED
465
+ ? (providerState || 'UNKNOWN')
466
+ : (freshness || providerState || 'UNKNOWN');
467
+ const reason = status === RETRIEVED ? null
468
+ : indexState === 'STALE' ? 'index-stale'
469
+ : indexState === 'UNKNOWN' ? 'index-unverifiable' : 'empty-answer';
470
+ const n = need.attempts.reduce((max , entry ) => Math.max(max, entry.n || 0), 0) + 1;
471
+ need.attempts.push({
472
+ n, provider, capability: need.capability,
473
+ role: provider === null || provider === need.provider ? 'primary' : 'fallback',
474
+ status, reason, note: null, indexState,
475
+ attemptRef: current.attemptRef || null,
476
+ evidenceIds: group.map((item ) => item.id),
477
+ recordedAt: new Date().toISOString(), recordedBy: 'derived',
478
+ ...(reached.has(need.intent) && group.length === 0 ? { basis: 'report-need' } : {}),
479
+ });
480
+ }
481
+ const outcome = deriveOutcome(need);
482
+ need.result = outcome.result;
483
+ need.resolvedBy = outcome.resolvedBy;
484
+ need.viaFallback = outcome.viaFallback;
485
+ need.evidenceIds = need.attempts.filter((entry ) => entry.status === RETRIEVED
486
+ && !(entry.recordedBy === 'derived' && (!Array.isArray(entry.evidenceIds) || entry.evidenceIds.length === 0)))
487
+ .flatMap((entry ) => entry.evidenceIds || []);
488
+ }
489
+ record = current;
490
+ } else {
491
+ for (const need of current.needs) {
492
+ need.evidenceIds = evidenceIdsFor(need, items);
493
+ if (need.evidenceIds.length > 0) need.result = RETRIEVED;
494
+ else if (reached.has(need.intent)) need.result = resultForEmptyAnswer(freshnessOf(current, need));
495
+ else need.result = UNREACHED;
496
+ }
497
+ }
498
+ current.reportedAt = new Date().toISOString();
499
+ record = current;
500
+ return current;
501
+ });
502
+ } catch (error ) {
503
+ return usageError('retrieval-plan', error.message, json, error);
504
+ }
409
505
 
410
506
  // Derived views, computed at emit and never stored on a need — a need holds a provider id and
411
507
  // nothing about that provider's index (plan D7).
412
- const byResult = (result: any) => record.needs.filter((need: any) => need.result === result).map((need: any) => need.intent);
413
- const stale = record.needs.filter((need: any) => freshnessOf(record, need) === 'STALE').map((need: any) => need.intent);
508
+ const byResult = (result ) => record.needs.filter((need ) => need.result === result).map((need ) => need.intent);
509
+ const stale = record.needs.filter((need ) => freshnessOf(record, need) === 'STALE').map((need ) => need.intent);
414
510
  const unreached = byResult(UNREACHED);
415
511
  const unverified = byResult(UNVERIFIED);
416
512
  const incomplete = unreached.length > 0 || unverified.length > 0;
@@ -430,6 +526,18 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
430
526
  unreached,
431
527
  unverified,
432
528
  stale,
529
+ ...(record.planId ? {
530
+ planId: record.planId,
531
+ binding: record.binding,
532
+ unbound: {
533
+ count: binding.unbound.length,
534
+ ids: binding.unbound.slice(0, 5).map((item ) => item.id),
535
+ causes: binding.unbound.reduce((counts , item ) => {
536
+ counts[item.cause] = (counts[item.cause] || 0) + 1;
537
+ return counts;
538
+ }, {}),
539
+ },
540
+ } : {}),
433
541
  }, null, 2));
434
542
  return finishRuntime(incomplete ? 1 : 0);
435
543
  }
@@ -447,8 +555,21 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
447
555
  + freshness.padEnd(16)
448
556
  + need.result + (freshness === 'STALE' ? ' (stale index)' : ''),
449
557
  );
558
+ if (record.planId) {
559
+ const attempts = need.attempts || [];
560
+ if (attempts.length > 1 || (attempts.length === 1 && attempts[0].status !== RETRIEVED)) {
561
+ for (const attempt of attempts) {
562
+ console.log(` Attempt #${attempt.n} ${attempt.role} ${attempt.provider} — ${attempt.status}`
563
+ + (attempt.reason ? ` (${attempt.reason})` : '')
564
+ + (attempt.evidenceIds?.length ? ` [${attempt.evidenceIds.join(', ')}]` : ''));
565
+ }
566
+ }
567
+ }
450
568
  }
451
569
  printProviderIndexes(record);
570
+ if (record.planId && binding.unbound.length > 0) {
571
+ console.log(`${binding.unbound.length} item(s) not bound to a declared need are not counted; re-record them with --need <intent>.`);
572
+ }
452
573
  console.log('─'.repeat(78));
453
574
  // NFR-004: the skip is stated as prominently as the failure, on its own line and named.
454
575
  if (unreached.length > 0) {
@@ -464,9 +585,209 @@ function reportPlan({ projectRoot, taskId, reached, json, slug }: any) {
464
585
  return finishRuntime(incomplete ? 1 : 0);
465
586
  }
466
587
 
588
+ /**
589
+ * `--action attempt` — record an attempt against a declared need (IC-012, IC-013).
590
+ *
591
+ * Rules:
592
+ * - The action appends one attempt to the need, under the plan lock (IC-020).
593
+ * - `indexState` is re-probed for `--provider` with `freshnessEntry` at record time.
594
+ * - `--status EMPTY` is mapped through `resultForEmptyAnswer(indexState)`, so an EMPTY
595
+ * from an UNKNOWN index is stored as UNVERIFIED with reason `index-unverifiable`.
596
+ * - The action writes no evidence and never changes declared fields.
597
+ * - Exits 0 when written, 2 on any refusal.
598
+ *
599
+ * @returns {number} exit code
600
+ */
601
+ function attemptPlan({
602
+ router,
603
+ projectRoot,
604
+ taskId,
605
+ intents,
606
+ provider,
607
+ attemptStatus,
608
+ reason,
609
+ note,
610
+ json,
611
+ slug,
612
+ sourceRoot,
613
+ } ) {
614
+ if (intents.length !== 1) {
615
+ return usageError(
616
+ 'retrieval-plan',
617
+ `--need takes one intent on retrieval-plan --action attempt, got ${intents.length}. Record one write per need.`,
618
+ json,
619
+ );
620
+ }
621
+ const intent = intents[0];
622
+
623
+ let record ;
624
+ try {
625
+ record = readPlan(projectRoot, taskId, slug);
626
+ } catch (error ) {
627
+ return usageError('retrieval-plan', error.message, json, error);
628
+ }
629
+
630
+ const resolved = resolveNeed(record, intent, taskId);
631
+ if (!resolved.ok) {
632
+ return usageError('retrieval-plan', resolved.message, json);
633
+ }
634
+ const declaredNeed = resolved.need;
635
+
636
+ if (attemptStatus === undefined || attemptStatus === null || attemptStatus === '') {
637
+ return usageError('retrieval-plan', '--status is required: EMPTY, UNREACHED or UNVERIFIED', json);
638
+ }
639
+
640
+ if (attemptStatus === 'RETRIEVED') {
641
+ return usageError(
642
+ 'retrieval-plan',
643
+ `RETRIEVED is never recorded by hand: it is derived from evidence bound to the need. Record what was found with: doflow evidence --task-id ${taskId} --action add --need ${intent} ...`,
644
+ json,
645
+ );
646
+ }
647
+
648
+ if (attemptStatus !== 'EMPTY' && attemptStatus !== 'UNREACHED' && attemptStatus !== 'UNVERIFIED') {
649
+ return usageError(
650
+ 'retrieval-plan',
651
+ `unknown --status '${attemptStatus}'. Valid here: EMPTY, UNREACHED, UNVERIFIED. A provider that could not be asked is UNREACHED with --reason provider-unavailable or policy-bypass.`,
652
+ json,
653
+ );
654
+ }
655
+
656
+ const REASON_CODES_LIST = ATTEMPT_REASON_CODES.join(', ');
657
+
658
+ if (reason === undefined || reason === null || reason === '') {
659
+ return usageError(
660
+ 'retrieval-plan',
661
+ `--status ${attemptStatus} needs --reason <code>: a lookup that returned no locator must say why. Codes: ${REASON_CODES_LIST}.`,
662
+ json,
663
+ );
664
+ }
665
+
666
+ if (!ATTEMPT_REASON_CODES.includes(reason)) {
667
+ return usageError(
668
+ 'retrieval-plan',
669
+ `unknown --reason '${reason}'. Codes: ${REASON_CODES_LIST}.`,
670
+ json,
671
+ );
672
+ }
673
+
674
+ const allowedReasons = {
675
+ EMPTY: ['empty-answer', 'index-stale', 'index-unverifiable'],
676
+ UNREACHED: ['provider-unavailable', 'policy-bypass', 'provider-error', 'timeout'],
677
+ UNVERIFIED: ['index-unverifiable'],
678
+ };
679
+ if (!allowedReasons[attemptStatus].includes(reason)) {
680
+ return usageError('retrieval-plan', `--reason '${reason}' is not allowed for --status ${attemptStatus}. Allowed: ${allowedReasons[attemptStatus].join(', ')}.`, json);
681
+ }
682
+
683
+ const capEntry = router.capabilities ? router.capabilities[declaredNeed.capability] : null;
684
+ const registeredProviders = capEntry && Array.isArray(capEntry.providers)
685
+ ? capEntry.providers.map((p ) => p.id).filter(Boolean)
686
+ : [];
687
+ const regList = registeredProviders.join(', ');
688
+
689
+ if (provider === undefined || provider === null || String(provider).trim() === '') {
690
+ return usageError(
691
+ 'retrieval-plan',
692
+ `--provider is required: an attempt is one provider's try at one need. Providers registered under ${declaredNeed.capability}: ${regList}.`,
693
+ json,
694
+ );
695
+ }
696
+
697
+ if (!registeredProviders.includes(provider)) {
698
+ return usageError(
699
+ 'retrieval-plan',
700
+ `provider '${provider}' is not registered under capability '${declaredNeed.capability}' (need '${intent}'). Registered: ${regList}. A fallback is another provider of the same capability, for example native.rg.`,
701
+ json,
702
+ );
703
+ }
704
+
705
+ if (note !== undefined && note !== null && String(note).length > 160) {
706
+ return usageError(
707
+ 'retrieval-plan',
708
+ `--note is at most 160 characters (got ${String(note).length}): it is a hint for a reader, and the runtime never reads it.`,
709
+ json,
710
+ );
711
+ }
712
+
713
+ const sourceModeOf = sourceModeLookup(router.capabilities);
714
+ const probe = freshnessEntry(provider, sourceRoot, sourceModeOf);
715
+ const indexState = probe.state;
716
+
717
+ let finalStatus = attemptStatus;
718
+ let finalReason = reason;
719
+
720
+ if (attemptStatus === 'EMPTY') {
721
+ finalStatus = resultForEmptyAnswer(indexState);
722
+ if (finalStatus === 'UNVERIFIED') {
723
+ finalReason = 'index-unverifiable';
724
+ } else if (indexState === 'STALE') {
725
+ finalReason = 'index-stale';
726
+ }
727
+ }
728
+
729
+ const activeRef = activeAttemptRef(projectRoot, taskId, slug);
730
+ let savedAttempt = null;
731
+ let file ;
732
+ try {
733
+ file = updatePlan(projectRoot, taskId, slug, (disk ) => {
734
+ const current = requirePlanForUpdate(disk, taskId);
735
+ const targetNeed = current.needs ? current.needs.find((n ) => n.intent === intent) : null;
736
+ if (!targetNeed) {
737
+ throw new Error(`need '${intent}' missing from disk plan during update`);
738
+ }
739
+ if (!Array.isArray(targetNeed.attempts)) {
740
+ targetNeed.attempts = [];
741
+ }
742
+ const n = targetNeed.attempts.length + 1;
743
+ const role = provider === targetNeed.provider ? 'primary' : 'fallback';
744
+ const attempt = {
745
+ n,
746
+ provider,
747
+ capability: targetNeed.capability,
748
+ role,
749
+ status: finalStatus,
750
+ reason: finalReason,
751
+ note: note !== undefined && note !== null ? String(note) : null,
752
+ indexState,
753
+ attemptRef: activeRef,
754
+ evidenceIds: [],
755
+ recordedAt: new Date().toISOString(),
756
+ recordedBy: 'caller',
757
+ };
758
+ targetNeed.attempts.push(attempt);
759
+ savedAttempt = attempt;
760
+ return current;
761
+ });
762
+ } catch (error ) {
763
+ return usageError('retrieval-plan', error.message, json, error);
764
+ }
765
+
766
+ if (json) {
767
+ console.log(JSON.stringify({
768
+ action: 'attempt',
769
+ taskId,
770
+ intent,
771
+ stateFile: file,
772
+ attempt: savedAttempt,
773
+ }, null, 2));
774
+ return finishRuntime(0);
775
+ }
776
+
777
+ console.log(`\nDoFlow Retrieval Attempt recorded [Task: ${taskId}]:`);
778
+ console.log('═'.repeat(78));
779
+ console.log(`Need: ${intent} Attempt: #${savedAttempt.n} Role: ${savedAttempt.role}`);
780
+ console.log(`Provider: ${savedAttempt.provider} Status: ${savedAttempt.status} Reason: ${savedAttempt.reason}`);
781
+ if (savedAttempt.note) console.log(`Note: ${savedAttempt.note}`);
782
+ console.log('─'.repeat(78));
783
+ console.log(`Recorded attempt #${savedAttempt.n} to ${file}`);
784
+ console.log('═'.repeat(78) + '\n');
785
+ return finishRuntime(0);
786
+ }
787
+
467
788
  /** The provider index block both actions print: state, what was located, and the remedy. */
468
- function printProviderIndexes(record: any) {
469
- const entries = Object.entries<any>(record.providers || {});
789
+ function printProviderIndexes(record ) {
790
+ const entries = Object.entries (record.providers || {});
470
791
  if (entries.length === 0) return;
471
792
  console.log('─'.repeat(78));
472
793
  console.log('Provider indexes (probed once each, at declare time):');
@@ -481,9 +802,9 @@ function printProviderIndexes(record: any) {
481
802
  // ── the verb ──────────────────────────────────────────────────────────────────────────────────
482
803
 
483
804
  /** `--need a,b --need c` and `--need a --need b` are the same list. */
484
- function parseIntents(raw: any) {
805
+ function parseIntents(raw ) {
485
806
  const list = Array.isArray(raw) ? raw : (raw === undefined || raw === null ? [] : [raw]);
486
- const out: any = [];
807
+ const out = [];
487
808
  for (const entry of list) {
488
809
  for (const token of String(entry).split(',')) {
489
810
  const trimmed = token.trim();
@@ -504,18 +825,19 @@ function parseIntents(raw: any) {
504
825
  * @param {string} [options.stage] the stage id declaring the plan
505
826
  * @param {boolean} [options.json=false]
506
827
  * @param {string} [options.repoRoot] the DoFlow package root, holding the capability registries
507
- * @param {string} [options.stateRoot] the project the task belongs to; its state and its index
828
+ * @param {string} [options.stateRoot] the project the task belongs to; its state
829
+ * @param {string} [options.sourceRoot] the checkout whose source and index freshness are measured
508
830
  * @returns {number} exit code
509
831
  */
510
- function handleRetrievalPlanCommand(options: any = {}) {
511
- const { taskId, action = 'report', need, stage, json = false, repoRoot, stateRoot, slug } = options;
832
+ function handleRetrievalPlanCommand(options = {}) {
833
+ const { taskId, action = 'report', need, stage, provider, attemptStatus, reason, note, json = false, repoRoot, stateRoot, sourceRoot: requestedSourceRoot, slug } = options;
512
834
 
513
835
  // NFR-001, at the boundary where a caller's input first becomes a record. `bin/doflow.js` already
514
836
  // refuses every score-shaped flag name on the command line; this is the same rule applied to the
515
837
  // option object, so a second input surface cannot arrive later without inheriting the refusal.
516
838
  try {
517
839
  assertNoScoreFields(options, '');
518
- } catch (error: any) {
840
+ } catch (error ) {
519
841
  return usageError('retrieval-plan', error.message, json, error);
520
842
  }
521
843
 
@@ -523,18 +845,19 @@ function handleRetrievalPlanCommand(options: any = {}) {
523
845
  // accepts it as an alias for its own default rather than reporting a usage error on an argument
524
846
  // the caller never typed — `verify` and `claim` already do exactly this.
525
847
  const resolvedAction = action === 'status' ? 'report' : action;
526
- if (resolvedAction !== 'declare' && resolvedAction !== 'report') {
527
- return usageError('retrieval-plan', `unknown --action '${action}'. Valid: declare, report (default)`, json);
848
+ if (resolvedAction !== 'declare' && resolvedAction !== 'report' && resolvedAction !== 'attempt') {
849
+ return usageError('retrieval-plan', `unknown --action '${action}'. Valid: declare, report (default), attempt`, json);
528
850
  }
529
851
 
530
852
  // The project the task belongs to owns both the state and the index whose age is being measured.
531
853
  // The package root owns the capability registries, and is a different directory in every install.
532
854
  const projectRoot = stateRoot || process.cwd();
855
+ const sourceRoot = requestedSourceRoot || projectRoot;
533
856
  const packageRoot = repoRoot || REPO_ROOT;
534
857
 
535
858
  try {
536
859
  assertSafeTaskId(taskId);
537
- } catch (error: any) {
860
+ } catch (error ) {
538
861
  return usageError('retrieval-plan', error.message, json, error);
539
862
  }
540
863
 
@@ -552,13 +875,35 @@ function handleRetrievalPlanCommand(options: any = {}) {
552
875
  let router;
553
876
  try {
554
877
  router = new CapabilityRouter({ repoRoot: packageRoot });
555
- } catch (error: any) {
878
+ } catch (error ) {
879
+ return usageError('retrieval-plan', error.message, json, error);
880
+ }
881
+ return declarePlan({ router, projectRoot, sourceRoot, taskId, stage, intents, json, slug });
882
+ }
883
+
884
+ if (resolvedAction === 'attempt') {
885
+ let router;
886
+ try {
887
+ router = new CapabilityRouter({ repoRoot: packageRoot });
888
+ } catch (error ) {
556
889
  return usageError('retrieval-plan', error.message, json, error);
557
890
  }
558
- return declarePlan({ router, projectRoot, taskId, stage, intents, json, slug });
891
+ return attemptPlan({
892
+ router,
893
+ projectRoot,
894
+ sourceRoot,
895
+ taskId,
896
+ intents,
897
+ provider,
898
+ attemptStatus,
899
+ reason,
900
+ note,
901
+ json,
902
+ slug,
903
+ });
559
904
  }
560
905
 
561
- return reportPlan({ projectRoot, taskId, reached: new Set(intents), json, slug });
906
+ return reportPlan({ projectRoot, sourceRoot, taskId, reached: new Set(intents), json, slug, repoRoot: packageRoot });
562
907
  }
563
908
 
564
909
  module.exports = {
@@ -575,4 +920,8 @@ module.exports = {
575
920
  NOT_APPLICABLE,
576
921
  FRESHNESS_STATES,
577
922
  EMPTY_ANSWER_BY_FRESHNESS,
923
+ ATTEMPT_REASON_CODES,
924
+ applyReport: (disk , taskId ) => requirePlanForUpdate(disk, taskId),
925
+ applyAttempt: (disk , taskId ) => requirePlanForUpdate(disk, taskId),
926
+ _probe: (providerId , root , sourceModeOf ) => freshnessEntry(providerId, root, sourceModeOf),
578
927
  };