@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,7 +4,7 @@
4
4
 
5
5
  const fs = require('node:fs');
6
6
  const path = require('node:path');
7
- const { resolveActiveFeature } = require('./feature-resolve.ts');
7
+ const featureResolve = require('./feature-resolve.js');
8
8
 
9
9
  // Which feature a task record belongs to (feature 045, IC-001/IC-002).
10
10
  //
@@ -19,7 +19,7 @@ const SAFE_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
19
19
  const SLUG_RULE = 'use letters, digits, dot, underscore or dash, start with a letter or digit, and no ".."';
20
20
 
21
21
  /** The one shape check for a feature slug (IC-009); do-paths.sh applies the same rule in bash. */
22
- function isSafeSlug(slug: any): boolean {
22
+ function isSafeSlug(slug ) {
23
23
  return typeof slug === 'string' && SAFE_SLUG.test(slug) && !slug.includes('..');
24
24
  }
25
25
 
@@ -29,31 +29,56 @@ function isSafeSlug(slug: any): boolean {
29
29
  * @param {*} slug
30
30
  * @returns {{error: 'invalid-slug', message: string, hint: string}|null}
31
31
  */
32
- function invalidSlugRefusal(slug: any): { error: 'invalid-slug'; message: string; hint: string } | null {
32
+ function invalidSlugRefusal(slug ) {
33
33
  if (typeof slug !== 'string' || slug === '' || isSafeSlug(slug)) return null;
34
34
  return { error: 'invalid-slug', message: `slug "${slug}" is not a valid feature slug: ${SLUG_RULE}`, hint: SLUG_RULE };
35
35
  }
36
36
 
37
37
  /** (projectRoot, slug) -> { slug, hasRegister } | { reason }. One resolver spawn per key per process. */
38
- const featureCache = new Map<string, any>();
38
+ const featureCache = new Map ();
39
+
40
+ /** The resolver's raw answer, including thrown failures, shared by every consumer in this process. */
41
+ const resolvedFeatureCache = new Map ();
39
42
 
40
43
  /** A slug the CLI named explicitly (`--slug`), applied to every store call in this process that was
41
44
  * not given its own. The dispatcher sets it once so the verbs whose handlers build their ledger
42
45
  * internally (readiness, evidence) route the same way as the ones that take `slug` directly. */
43
- let defaultSlug: string | null = null;
46
+ let defaultSlug = null;
44
47
 
45
- function setDefaultSlug(slug: any): void {
48
+ function setDefaultSlug(slug ) {
46
49
  defaultSlug = typeof slug === 'string' && slug !== '' ? slug : null;
47
50
  }
48
51
 
49
- function clearTaskScopeCache(): void {
52
+ function clearTaskScopeCache() {
50
53
  featureCache.clear();
54
+ resolvedFeatureCache.clear();
55
+ }
56
+
57
+ /**
58
+ * Resolve once per process per (root, slug). The resolver spawns do-paths.sh, and every store and
59
+ * the decisions lookup must see the same answer.
60
+ */
61
+ function resolveFeature({ projectRoot, slug = null } ) {
62
+ const key = `${path.resolve(projectRoot)}\0${slug || ''}`;
63
+ if (resolvedFeatureCache.has(key)) {
64
+ const cached = resolvedFeatureCache.get(key) ;
65
+ if ('error' in cached) throw cached.error;
66
+ return cached.value;
67
+ }
68
+ try {
69
+ const value = featureResolve.resolveActiveFeature({ projectRoot, slug: slug || null });
70
+ resolvedFeatureCache.set(key, { value });
71
+ return value;
72
+ } catch (error) {
73
+ resolvedFeatureCache.set(key, { error });
74
+ throw error;
75
+ }
51
76
  }
52
77
 
53
78
  /** The resolver reports the git top level, which is the project root or one of its ancestors, and
54
79
  * the feature folders live directly under it — so no `agent-docs/doflow` on the way up means there
55
80
  * is nothing to resolve and the spawn is skipped. */
56
- function hasFeatureFolderAbove(projectRoot: string): boolean {
81
+ function hasFeatureFolderAbove(projectRoot ) {
57
82
  let dir = path.resolve(projectRoot);
58
83
  for (;;) {
59
84
  try {
@@ -65,13 +90,13 @@ function hasFeatureFolderAbove(projectRoot: string): boolean {
65
90
  }
66
91
  }
67
92
 
68
- function featureFor(projectRoot: string, slug: string | null | undefined): any {
93
+ function featureFor(projectRoot , slug ) {
69
94
  const key = `${path.resolve(projectRoot)}\0${slug || ''}`;
70
95
  if (featureCache.has(key)) return featureCache.get(key);
71
- let value: any = { reason: 'no-feature' };
96
+ let value = { reason: 'no-feature' };
72
97
  try {
73
98
  if (hasFeatureFolderAbove(projectRoot)) {
74
- const found = resolveActiveFeature({ projectRoot, slug: slug || null });
99
+ const found = resolveFeature({ projectRoot, slug: slug || null });
75
100
  if (!found.error && found.paths && isSafeSlug(found.paths.feature_slug)) {
76
101
  value = { slug: found.paths.feature_slug, hasRegister: found.paths.has_decisions === true, exists: fs.existsSync(found.featureDir) };
77
102
  }
@@ -90,7 +115,7 @@ function featureFor(projectRoot: string, slug: string | null | undefined): any {
90
115
  * `reason` is `namespaced` when a namespace applies, else `no-feature`, `no-register` or
91
116
  * `feature-level-id`.
92
117
  */
93
- function resolveTaskScope({ projectRoot, taskId, slug = null }: { projectRoot: string; taskId: string; slug?: string | null }): { namespace: string | null; slug: string | null; reason: string } {
118
+ function resolveTaskScope({ projectRoot, taskId, slug = null } ) {
94
119
  const feature = featureFor(projectRoot, slug || defaultSlug);
95
120
  if (feature.reason) return { namespace: null, slug: null, reason: feature.reason };
96
121
  if (!feature.hasRegister) return { namespace: null, slug: feature.slug, reason: 'no-register' };
@@ -107,7 +132,7 @@ function resolveTaskScope({ projectRoot, taskId, slug = null }: { projectRoot: s
107
132
  * @param {string} options.slug
108
133
  * @returns {boolean}
109
134
  */
110
- function slugNamesNoFeature({ projectRoot, slug }: { projectRoot: string; slug: string }): boolean {
135
+ function slugNamesNoFeature({ projectRoot, slug } ) {
111
136
  const feature = featureFor(projectRoot, slug);
112
137
  return Boolean(feature.reason) || feature.exists === false;
113
138
  }
@@ -121,10 +146,10 @@ function slugNamesNoFeature({ projectRoot, slug }: { projectRoot: string; slug:
121
146
  * @param {string|null} [options.slug]
122
147
  * @returns {string}
123
148
  */
124
- function taskStoreDir({ projectRoot, store, taskId, slug = null }: { projectRoot: string; store: string; taskId: string; slug?: string | null }): string {
149
+ function taskStoreDir({ projectRoot, store, taskId, slug = null } ) {
125
150
  const base = path.join(projectRoot, '.doflow', 'state', store);
126
151
  const { namespace } = resolveTaskScope({ projectRoot, taskId, slug });
127
152
  return namespace ? path.join(base, namespace) : base;
128
153
  }
129
154
 
130
- module.exports = { resolveTaskScope, taskStoreDir, setDefaultSlug, clearTaskScopeCache, isSafeSlug, invalidSlugRefusal, slugNamesNoFeature };
155
+ module.exports = { resolveFeature, resolveTaskScope, taskStoreDir, setDefaultSlug, clearTaskScopeCache, isSafeSlug, invalidSlugRefusal, slugNamesNoFeature };
@@ -28,8 +28,8 @@ const { isDeepStrictEqual } = require('node:util');
28
28
 
29
29
  /** Three-way merge against the reader's snapshot. Unchanged local records never overwrite a
30
30
  * newer disk version; concurrent edits to the same record must be retried after an explicit read. */
31
- function mergeRecords(local: any[], disk: any[], baseline: Map<any, any>): any[] {
32
- const merged = new Map(disk.map((item: any) => [item.id, item]));
31
+ function mergeRecords(local , disk , baseline ) {
32
+ const merged = new Map(disk.map((item ) => [item.id, item]));
33
33
  for (const item of local) {
34
34
  const previous = baseline.get(item.id);
35
35
  const current = merged.get(item.id);
@@ -48,7 +48,7 @@ const LOCK_RETRIES = 250;
48
48
  const LOCK_WAIT_MS = 20;
49
49
 
50
50
  /** Synchronous sleep without spinning: Atomics.wait on a throwaway buffer. */
51
- function sleep(ms: number): void {
51
+ function sleep(ms ) {
52
52
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
53
53
  }
54
54
 
@@ -59,28 +59,28 @@ function sleep(ms: number): void {
59
59
  * @param {string} file
60
60
  * @returns {(() => void)|null}
61
61
  */
62
- function tryAcquireLock(fsImpl: any, file: string): (() => void) | null {
62
+ function tryAcquireLock(fsImpl , file ) {
63
63
  const lockDir = `${file}.lock`;
64
64
  const release = () => { try { fsImpl.rmdirSync(lockDir); } catch { /* released is released */ } };
65
65
  try {
66
66
  fsImpl.mkdirSync(lockDir);
67
67
  return release;
68
- } catch (error: any) {
68
+ } catch (error ) {
69
69
  if (error.code !== 'EEXIST') throw error;
70
70
  }
71
- let lock: any;
72
- try { lock = fsImpl.statSync(lockDir); } catch (error: any) {
71
+ let lock ;
72
+ try { lock = fsImpl.statSync(lockDir); } catch (error ) {
73
73
  if (error.code !== 'ENOENT') return null;
74
- try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError: any) {
74
+ try { fsImpl.mkdirSync(lockDir); return release; } catch (retryError ) {
75
75
  if (retryError.code === 'EEXIST') return null;
76
76
  throw retryError;
77
77
  }
78
78
  }
79
79
  if (Date.now() - lock.mtimeMs <= LOCK_STALE_MS) return null;
80
- try { fsImpl.rmdirSync(lockDir); } catch (error: any) {
80
+ try { fsImpl.rmdirSync(lockDir); } catch (error ) {
81
81
  if (error.code !== 'ENOENT') return null;
82
82
  }
83
- try { fsImpl.mkdirSync(lockDir); return release; } catch (error: any) {
83
+ try { fsImpl.mkdirSync(lockDir); return release; } catch (error ) {
84
84
  if (error.code === 'EEXIST') return null;
85
85
  throw error;
86
86
  }
@@ -93,7 +93,7 @@ function tryAcquireLock(fsImpl: any, file: string): (() => void) | null {
93
93
  * @param {string} file
94
94
  * @returns {() => void}
95
95
  */
96
- function acquireLock(fsImpl: any, file: string): () => void {
96
+ function acquireLock(fsImpl , file ) {
97
97
  const lockDir = `${file}.lock`;
98
98
  // The stale rule below is wall-clock, so the wait must be too: on a host where each attempt costs
99
99
  // real time, counting attempts alone would outlive a live holder's staleness window and then
@@ -117,25 +117,66 @@ function acquireLock(fsImpl: any, file: string): () => void {
117
117
  + 'If no other session is running, the lock is leftover from a crash younger than its stale timeout; retry shortly.');
118
118
  }
119
119
 
120
+ /**
121
+ * Takes the cross-process lock for one state file with a bounded wait.
122
+ * Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5).
123
+ * Throws an Error('store-locked') after one try plus retries.
124
+ * @param {Object} fsImpl
125
+ * @param {string} file
126
+ * @param {Object} [options]
127
+ * @param {number} [options.retries=3]
128
+ * @param {number} [options.waitMs=5]
129
+ * @returns {() => void} release function
130
+ */
131
+ function boundedAcquire(
132
+ fsImpl ,
133
+ file ,
134
+ options
135
+
136
+
137
+
138
+
139
+ = {}
140
+ ) {
141
+ const retries = options?.retries ?? 3;
142
+ const waitMs = options?.waitMs ?? 5;
143
+ const sleepFn = options?.sleepFn ?? sleep;
144
+ const tryAcquire = options?.tryAcquireLockFn ?? tryAcquireLock;
145
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
146
+ const release = tryAcquire(fsImpl, file);
147
+ if (release) return release;
148
+ if (attempt < retries && waitMs > 0) {
149
+ sleepFn(waitMs);
150
+ }
151
+ }
152
+ const error = new Error('store-locked');
153
+ (error ).code = 'store-locked';
154
+ throw error;
155
+ }
156
+
120
157
  /**
121
158
  * Reads and validates one task-state file. Returns null when the file does not exist.
122
159
  * @param {Object} fsImpl
123
160
  * @param {string} file
124
161
  * @returns {Object|null} the parsed payload
125
162
  */
126
- function readTaskState(fsImpl: any, file: string): any {
163
+ function readTaskState(fsImpl , file ) {
127
164
  if (!fsImpl.existsSync(file)) return null;
128
- let data: any;
165
+ let data ;
129
166
  try {
130
167
  data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
131
- } catch (error: any) {
132
- throw new Error(`Failed to load state file '${file}': ${error.message}. The write path is atomic `
168
+ } catch (error ) {
169
+ const failure = new Error(`Failed to load state file '${file}': ${error.message}. The write path is atomic `
133
170
  + '(tmp + rename), so a torn file means outside interference; recover it from version control '
134
171
  + 'or move it aside and re-record the task\'s items.');
172
+ failure.code = 'store-unreadable';
173
+ throw failure;
135
174
  }
136
175
  if (data && typeof data === 'object' && data.version !== undefined && data.version !== SCHEMA_VERSION) {
137
- throw new Error(`State file '${file}' has schema version ${data.version}; this runtime reads version ${SCHEMA_VERSION}. `
176
+ const failure = new Error(`State file '${file}' has schema version ${data.version}; this runtime reads version ${SCHEMA_VERSION}. `
138
177
  + 'Refusing to guess at its shape — upgrade the runtime or migrate the file.');
178
+ failure.code = 'store-unreadable';
179
+ throw failure;
139
180
  }
140
181
  return data;
141
182
  }
@@ -151,16 +192,32 @@ function readTaskState(fsImpl: any, file: string): any {
151
192
  * return the complete new payload (without `version`/`revision`, which are stamped here)
152
193
  * @param {() => void} [options.prepare] called INSIDE the lock before the file is read, for a store
153
194
  * that must inspect or set aside what is there under the same exclusion as the write
154
- * @returns {string} the file path written
195
+ * @param {(fsImpl: Object, file: string) => () => void} [options.acquire] optional lock acquirer
196
+ * defaulting to acquireLock
197
+ * @returns {string|null} the file path written, or null when build declines a write
155
198
  */
156
- function updateTaskState({ fsImpl, file, build, prepare }: { fsImpl: any; file: string; build: (disk: any) => any; prepare?: () => void }): string {
199
+ function updateTaskState({
200
+ fsImpl,
201
+ file,
202
+ build,
203
+ prepare,
204
+ acquire = acquireLock,
205
+ }
206
+
207
+
208
+
209
+
210
+
211
+ ) {
157
212
  fsImpl.mkdirSync(path.dirname(file), { recursive: true });
158
- const release = acquireLock(fsImpl, file);
213
+ const release = acquire(fsImpl, file);
159
214
  try {
160
215
  if (prepare) prepare();
161
216
  const disk = readTaskState(fsImpl, file);
217
+ const built = build(disk);
218
+ if (built === null) return null;
162
219
  const payload = {
163
- ...build(disk),
220
+ ...built,
164
221
  version: SCHEMA_VERSION,
165
222
  revision: (disk && Number.isInteger(disk.revision) ? disk.revision : 0) + 1,
166
223
  };
@@ -173,4 +230,12 @@ function updateTaskState({ fsImpl, file, build, prepare }: { fsImpl: any; file:
173
230
  }
174
231
  }
175
232
 
176
- module.exports = { acquireLock, tryAcquireLock, updateTaskState, readTaskState, mergeRecords, SCHEMA_VERSION };
233
+ module.exports = {
234
+ acquireLock,
235
+ tryAcquireLock,
236
+ boundedAcquire,
237
+ updateTaskState,
238
+ readTaskState,
239
+ mergeRecords,
240
+ SCHEMA_VERSION,
241
+ };
@@ -26,16 +26,16 @@
26
26
 
27
27
  const fs = require('node:fs');
28
28
  const path = require('node:path');
29
- const { formatMs, finish } = require('./render.ts');
29
+ const { formatMs, finish } = require('./render.js');
30
30
 
31
31
  const ORCHESTRATION_DIRNAME = 'orchestration';
32
32
 
33
33
  /** Mirrors resolveRunsLocation's shape, for the sibling state directory this reads. */
34
- function orchestrationDir(root: string): string {
34
+ function orchestrationDir(root ) {
35
35
  return path.join(root, '.doflow', 'state', ORCHESTRATION_DIRNAME);
36
36
  }
37
37
 
38
- function parseAt(value: any): number | null {
38
+ function parseAt(value ) {
39
39
  const ms = Date.parse(value);
40
40
  return Number.isNaN(ms) ? null : ms;
41
41
  }
@@ -43,10 +43,10 @@ function parseAt(value: any): number | null {
43
43
  /** Every record in a directory, plus the names of the ones that did not parse. A file that cannot be
44
44
  * read is counted and named rather than skipped: a report that quietly drops what it could not read
45
45
  * overstates its own coverage, which is the defect `malformedLines` already guards against for runs. */
46
- function readRecords(dir: string, fsImpl: any = fs): any {
46
+ function readRecords(dir , fsImpl = fs) {
47
47
  if (!fsImpl.existsSync(dir)) return { records: [], unreadable: [], exists: false };
48
- const records: any[] = [];
49
- const unreadable: any[] = [];
48
+ const records = [];
49
+ const unreadable = [];
50
50
  for (const name of fsImpl.readdirSync(dir).sort()) {
51
51
  if (!name.endsWith('.json')) continue;
52
52
  try {
@@ -56,7 +56,7 @@ function readRecords(dir: string, fsImpl: any = fs): any {
56
56
  continue;
57
57
  }
58
58
  records.push(parsed);
59
- } catch (err: any) {
59
+ } catch (err ) {
60
60
  unreadable.push({ file: name, reason: err.message });
61
61
  }
62
62
  }
@@ -65,11 +65,11 @@ function readRecords(dir: string, fsImpl: any = fs): any {
65
65
 
66
66
  /** A stage row is "not work done" when the orchestrator backfilled it or imported it rather than
67
67
  * executing it. Its duration measures when the record was written, so it is listed but never averaged. */
68
- function isSynthetic(entry: any): boolean {
68
+ function isSynthetic(entry ) {
69
69
  return Boolean(entry.backfilled) || (entry.executionStatus && entry.executionStatus !== 'completed');
70
70
  }
71
71
 
72
- function median(values: number[]): number | null {
72
+ function median(values ) {
73
73
  if (!values.length) return null;
74
74
  const sorted = [...values].sort((a, b) => a - b);
75
75
  const mid = Math.floor(sorted.length / 2);
@@ -77,11 +77,11 @@ function median(values: number[]): number | null {
77
77
  }
78
78
 
79
79
  /** One run's rows, walking history in recorded order with a cursor so no interval is counted twice. */
80
- function runRows(record: any): any {
80
+ function runRows(record ) {
81
81
  const history = Array.isArray(record.history) ? record.history : [];
82
82
  let cursor = parseAt(record.startedAt);
83
- const stages: any[] = [];
84
- const gates: any[] = [];
83
+ const stages = [];
84
+ const gates = [];
85
85
  let reruns = 0;
86
86
 
87
87
  for (const entry of history) {
@@ -131,13 +131,13 @@ function runRows(record: any): any {
131
131
  /** Median elapsed per stage id across runs of one class. Synthetic rows are excluded here and still
132
132
  * listed in their run: averaging the moment a file was written into the figure most likely to be
133
133
  * quoted would corrupt it. */
134
- function aggregateStages(runs: any[]): any[] {
135
- const perStage = new Map<string, number[]>();
134
+ function aggregateStages(runs ) {
135
+ const perStage = new Map ();
136
136
  for (const run of runs) {
137
137
  for (const row of run.stages) {
138
138
  if (row.synthetic || row.elapsedMs === null || !row.stage) continue;
139
139
  if (!perStage.has(row.stage)) perStage.set(row.stage, []);
140
- perStage.get(row.stage)!.push(row.elapsedMs);
140
+ perStage.get(row.stage) .push(row.elapsedMs);
141
141
  }
142
142
  }
143
143
  return [...perStage.entries()].map(([stage, values]) => ({
@@ -146,14 +146,14 @@ function aggregateStages(runs: any[]): any[] {
146
146
  }
147
147
 
148
148
  /** Pure. Reads nothing and writes nothing; takes already-parsed records. */
149
- function buildIndicators({ records = [], unreadable = [], exists = true } = {}): any {
150
- const runs = (records as any[]).map(runRows);
151
- const byClass = new Map<string, any>();
149
+ function buildIndicators({ records = [], unreadable = [], exists = true } = {}) {
150
+ const runs = (records ).map(runRows);
151
+ const byClass = new Map ();
152
152
 
153
153
  for (const run of runs) {
154
154
  const key = run.taskClass || '(unclassified)';
155
155
  if (!byClass.has(key)) byClass.set(key, { taskClass: key, runs: [], stageAggregate: [] });
156
- byClass.get(key)!.runs.push(run);
156
+ byClass.get(key) .runs.push(run);
157
157
  }
158
158
 
159
159
  for (const group of byClass.values()) group.stageAggregate = aggregateStages(group.runs);
@@ -176,7 +176,7 @@ function buildIndicators({ records = [], unreadable = [], exists = true } = {}):
176
176
  }
177
177
 
178
178
  /** One class block: its runs, each run's stage and gate rows, then the class median. */
179
- function printClass(group: any): void {
179
+ function printClass(group ) {
180
180
  console.log('─'.repeat(78));
181
181
  console.log(`CLASS: ${group.taskClass} (${group.runs.length} run(s))`);
182
182
  for (const run of group.runs) {
@@ -195,7 +195,7 @@ function printClass(group: any): void {
195
195
  }
196
196
  }
197
197
 
198
- function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {}): number {
198
+ function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } = {}) {
199
199
  const dir = orchestrationDir(projectRoot);
200
200
  const read = readRecords(dir);
201
201
  const view = buildIndicators(read);
@@ -220,7 +220,7 @@ function handleIndicatorsCommand({ json = false, projectRoot = process.cwd() } =
220
220
 
221
221
  console.log(`Runs: ${view.runCount} across ${view.byClass.length} task class(es)`);
222
222
  if (view.unreadable.length) {
223
- console.log(`Unreadable: ${view.unreadable.length} record(s) — ${view.unreadable.map((u: any) => u.file).join(', ')}`);
223
+ console.log(`Unreadable: ${view.unreadable.length} record(s) — ${view.unreadable.map((u ) => u.file).join(', ')}`);
224
224
  }
225
225
 
226
226
  for (const group of view.byClass) printClass(group);