@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
@@ -16,15 +16,15 @@
16
16
  // https://opencode.ai/docs/rules
17
17
  const fs = require('node:fs');
18
18
  const path = require('node:path');
19
- const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.ts');
20
- const { readMcpFiles, planMcpEntries, verifyMcpEntries, ownedMcpIds, writeMcpEntries } = require('../mcp-entries.ts');
19
+ const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.js');
20
+ const { readMcpFiles, planMcpEntries, verifyMcpEntries, ownedMcpIds, writeMcpEntries } = require('../mcp-entries.js');
21
21
 
22
22
  // Only the marker constants: marker-merge.js reads and writes files itself, which cannot be used
23
23
  // from plan(), whose contract is to compute changes without touching disk. The gemini adapter
24
24
  // solves this the same way, with a pure managedInstruction() over strings.
25
- const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.ts');
26
- const { destructiveCommandGlobs } = require('../../helper/guardrails.ts');
27
- const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.ts');
25
+ const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
26
+ const { destructiveCommandGlobs } = require('../../helper/guardrails.js');
27
+ const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
28
28
 
29
29
  const HARNESS = 'opencode';
30
30
  /**
@@ -48,7 +48,7 @@ const MCP_RENDERER = 'opencode-mcp';
48
48
  * historical shape.
49
49
  */
50
50
  function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
51
- function nativePaths({ scope, scopeRoot, homeDir }: any) {
51
+ function nativePaths({ scope, scopeRoot, homeDir } ) {
52
52
  return resolveHarnessPaths(declaredPaths, { scope, scopeRoot, homeDir });
53
53
  }
54
54
 
@@ -57,20 +57,20 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
57
57
  * `opencode.json` are read from the repo root, but the skills tree is namespaced under
58
58
  * `.opencode/` per design.md's native-surfaces table. Global scope has no such split —
59
59
  * `~/.config/opencode` already IS the skills root. Declared as `treeRoot` in harnesses.json. */
60
- function copyTreeConfigDir(paths: any) {
60
+ function copyTreeConfigDir(paths ) {
61
61
  return paths.treeRoot;
62
62
  }
63
63
 
64
64
  /** Agents live under `.opencode/agents/` at project scope and `~/.config/opencode/agents/`
65
65
  * globally — a different root from the skills tree in both scopes, so they get their own
66
66
  * destination resolution rather than reusing copyTreeConfigDir. Declared as `agentsDirectory`. */
67
- function agentsDestDir(paths: any) {
67
+ function agentsDestDir(paths ) {
68
68
  return paths.agentsDirectory;
69
69
  }
70
70
 
71
71
  /** Discovery keeps opencode.json as plan read it (`mcpSnapshot`), which verify compares against,
72
72
  * and reports the servers DoFlow owns in its `mcp` object now (`mcpOwned`). */
73
- function discover({ scope, scopeRoot, mcp = [], mcpAdoptable = [], ledger, context = {}, fsImpl = fs }: any) {
73
+ function discover({ scope, scopeRoot, mcp = [], mcpAdoptable = [], ledger, context = {}, fsImpl = fs } ) {
74
74
  const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir });
75
75
  const instruction = fsImpl.existsSync(paths.instruction) ? fsImpl.readFileSync(paths.instruction, 'utf8') : null;
76
76
  const entries = mcpEntriesInput({ paths, mcp, mcpAdoptable, ledger });
@@ -81,7 +81,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
81
81
  return { paths, instruction, config: readJson(paths.config, { fsImpl }), mcpSnapshot, mcpOwned: ownedMcpIds({ ...entries, files: mcpSnapshot }), mcpConfigured };
82
82
  }
83
83
 
84
- function render({ content = '' }: any = {}) {
84
+ function render({ content = '' } = {}) {
85
85
  return `${MARKER_START}\n${String(content).trimEnd()}\n${MARKER_END}\n`;
86
86
  }
87
87
 
@@ -90,17 +90,17 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
90
90
  * every config it discovers — replacing would silently drop a user's own entries. Skills are
91
91
  * deliberately absent here: they are a materialised directory now, discovered natively, not a
92
92
  * registered path. The `mcp` object is left to the entry changes. */
93
- function mergeConfig(existing: any) {
93
+ function mergeConfig(existing ) {
94
94
  const next = { ...(existing || {}) };
95
- const union = (current: any, add: any) => [...new Set([...(Array.isArray(current) ? current : []), ...add])];
95
+ const union = (current , add ) => [...new Set([...(Array.isArray(current) ? current : []), ...add])];
96
96
  next.instructions = union(next.instructions, [INSTRUCTION_FILE]);
97
97
  return next;
98
98
  }
99
99
 
100
100
  /** Remove only what mergeConfig added, leaving every user key and every unrelated array entry. */
101
- function unmergeConfig(existing: any) {
101
+ function unmergeConfig(existing ) {
102
102
  const next = { ...(existing || {}) };
103
- const without = (current: any, drop: any) => (Array.isArray(current) ? current.filter((v) => !drop.includes(v)) : []);
103
+ const without = (current , drop ) => (Array.isArray(current) ? current.filter((v) => !drop.includes(v)) : []);
104
104
  next.instructions = without(next.instructions, [INSTRUCTION_FILE]);
105
105
  if (!next.instructions.length) delete next.instructions;
106
106
  return next;
@@ -116,12 +116,12 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
116
116
  * comment-free glob we can re-derive) are touched; a user's own bash rules survive untouched.
117
117
  * Entries are namespaced by value, not by key, so ownership is decided by re-deriving the list
118
118
  * from the single authored source rather than by trusting whatever happens to be present. */
119
- function guardrailBashRules(context: any) {
119
+ function guardrailBashRules(context ) {
120
120
  const globs = destructiveCommandGlobs({ repoRoot: context.repoRoot });
121
- return Object.fromEntries(globs.map((glob: any) => [glob, 'deny']));
121
+ return Object.fromEntries(globs.map((glob ) => [glob, 'deny']));
122
122
  }
123
123
 
124
- function mergeGuardrails(existing: any, context: any) {
124
+ function mergeGuardrails(existing , context ) {
125
125
  const next = { ...(existing || {}) };
126
126
  const bash = { ...guardrailBashRules(context), ...((next.permission?.bash ?? {})) };
127
127
  // Keep user rules, but make sure ours win for identical globs (they are the stricter value).
@@ -129,7 +129,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
129
129
  return next;
130
130
  }
131
131
 
132
- function stripGuardrails(existing: any, context: any) {
132
+ function stripGuardrails(existing , context ) {
133
133
  if (!existing?.permission?.bash) return existing;
134
134
  const ours = new Set(Object.keys(guardrailBashRules(context)));
135
135
  const bash = Object.fromEntries(Object.entries(existing.permission.bash).filter(([glob]) => !ours.has(glob)));
@@ -141,7 +141,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
141
141
  return rest;
142
142
  }
143
143
 
144
- function managedInstruction(existing: any, rendered: any) {
144
+ function managedInstruction(existing , rendered ) {
145
145
  if (existing === null) return { ok: true, operation: 'create', content: rendered };
146
146
  const start = existing.indexOf(MARKER_START);
147
147
  const end = existing.indexOf(MARKER_END);
@@ -153,7 +153,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
153
153
 
154
154
  /** Strip DoFlow's span, leaving the rest of a shared AGENTS.md intact. Returns null when there is
155
155
  * nothing to do, so remove() can skip the write entirely. */
156
- function strippedInstruction(existing: any) {
156
+ function strippedInstruction(existing ) {
157
157
  if (typeof existing !== 'string') return null;
158
158
  const start = existing.indexOf(MARKER_START);
159
159
  const end = existing.indexOf(MARKER_END);
@@ -168,11 +168,11 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
168
168
  /** `agents.shared` projects onto OpenCode through its own renderer name (its frontmatter is
169
169
  * transformed into OpenCode's markdown-agent vocabulary) but materialises with exactly the same
170
170
  * engine and change records as every other copy-tree asset. */
171
- function opencodeTreeAssets(assets: any) {
172
- return [...copyTreeAssets(assets), ...(assets || []).filter((asset: any) => asset.renderer === 'opencode-agents')];
171
+ function opencodeTreeAssets(assets ) {
172
+ return [...copyTreeAssets(assets), ...(assets || []).filter((asset ) => asset.renderer === 'opencode-agents')];
173
173
  }
174
174
 
175
- function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs }: any) {
175
+ function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs } ) {
176
176
  const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir });
177
177
  const treeConfigDir = copyTreeConfigDir(paths);
178
178
  const changes = [];
@@ -190,10 +190,10 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
190
190
  // is never deleted on removal, forced or not.
191
191
  force: !removing && context?.force === true, });
192
192
  treeResults.push(result);
193
- conflicts.push(...result.conflicts.map((reason: any) => `${asset.id}: ${reason}`));
193
+ conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
194
194
  // A hand-edited file at a path DoFlow no longer writes is the user's: it stays, only its
195
195
  // ownership row is released.
196
- notices.push(...result.kept.map((item: any) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
196
+ notices.push(...result.kept.map((item ) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
197
197
  for (const change of result.changes) {
198
198
  changes.push({
199
199
  assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
@@ -209,7 +209,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
209
209
  return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
210
210
  }
211
211
 
212
- function applyCopyTreeAssets(changes: any, { fsImpl = fs }: any = {}) {
212
+ function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
213
213
  let applied = 0;
214
214
  const grouped = new Map();
215
215
  for (const change of changes) {
@@ -224,13 +224,13 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
224
224
  return applied;
225
225
  }
226
226
 
227
- function removeCopyTreeAssets(changes: any, { fsImpl = fs }: any = {}) {
228
- const treeChanges = changes.filter((change: any) => change.kind === 'copy-tree-file' && change.operation === 'remove' && !change.retained)
229
- .map((change: any) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
227
+ function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
228
+ const treeChanges = changes.filter((change ) => change.kind === 'copy-tree-file' && change.operation === 'remove' && !change.retained)
229
+ .map((change ) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
230
230
  return removeTree({ changes: treeChanges, fsImpl }).removed;
231
231
  }
232
232
 
233
- function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs }: any) {
233
+ function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs } ) {
234
234
  const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir });
235
235
  const treeConfigDir = copyTreeConfigDir(paths);
236
236
  const statuses = [];
@@ -240,7 +240,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
240
240
  const destDir = asset.renderer === 'opencode-agents' ? agentsDestDir(paths) : sharedTreeDestDir(paths.root, asset.nativeDir) ?? copyTreeDestDir(treeConfigDir, asset);
241
241
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'OpenCode');
242
242
  const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
243
- conflicts.push(...result.conflicts.map((reason: any) => `${asset.id}: ${reason}`));
243
+ conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
244
244
  for (const resource of result.resources) {
245
245
  resources.push({
246
246
  assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:${HARNESS}:copy-tree:${asset.id}:${resource.relPath}`,
@@ -259,45 +259,45 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
259
259
  /** OpenCode's settings-merge change has no asset of its own in core/registry/assets.json (it's a
260
260
  * registration side-effect of the instructions/mcp assets, not a projected asset), so it piggybacks
261
261
  * on a real asset id already routed to this harness — the same "pseudo-component" technique
262
- * src/adapters/gemini/index.ts#hooksAssetId and src/adapters/kiro/index.ts#pseudoAssetId use. The
262
+ * src/adapters/gemini/index.js#hooksAssetId and src/adapters/kiro/index.js#pseudoAssetId use. The
263
263
  * lifecycle layer validates every change's assetId against the harness's actual asset list, so an
264
264
  * invented id like the literal string 'opencode.config' is rejected as unknown. */
265
- function pseudoAssetId(assets: any) {
266
- return assets.find((asset: any) => asset.capability === 'instructions')?.id ?? assets[0]?.id;
265
+ function pseudoAssetId(assets ) {
266
+ return assets.find((asset ) => asset.capability === 'instructions')?.id ?? assets[0]?.id;
267
267
  }
268
268
 
269
269
  // ---- MCP entries (opencode.json `mcp`) ----
270
270
 
271
271
  /** OpenCode's own server shape: a local server whose command and arguments form one array. */
272
- function buildServerEntry(server: any) {
272
+ function buildServerEntry(server ) {
273
273
  return { type: 'local', command: [server.command, ...(server.args || [])], enabled: true };
274
274
  }
275
275
 
276
- function ownershipIdentity(id: any) { return `doflow:${HARNESS}:mcp-server:${id}`; }
276
+ function ownershipIdentity(id ) { return `doflow:${HARNESS}:mcp-server:${id}`; }
277
277
 
278
278
  /** Everything the shared entry-ownership rules (../mcp-entries.ts) need from this adapter. */
279
- function mcpEntriesInput({ paths, mcp = [], mcpAdoptable = [], ledger, assets = [] }: any) {
280
- const rendered = (servers: any) => servers.map((server: any) => ({ id: server.id, entry: buildServerEntry(server) }));
281
- const rows = (ledger?.resources || []).filter((row: any) => row.kind === 'mcp-server');
279
+ function mcpEntriesInput({ paths, mcp = [], mcpAdoptable = [], ledger, assets = [] } ) {
280
+ const rendered = (servers ) => servers.map((server ) => ({ id: server.id, entry: buildServerEntry(server) }));
281
+ const rows = (ledger?.resources || []).filter((row ) => row.kind === 'mcp-server');
282
282
  return {
283
283
  file: paths.config, selected: rendered(mcp), adoptable: rendered(mcpAdoptable),
284
- ownRows: rows.filter((row: any) => row.harness === HARNESS).map((row: any) => ({
284
+ ownRows: rows.filter((row ) => row.harness === HARNESS).map((row ) => ({
285
285
  identity: row.identity, target: row.target, fingerprint: row.fingerprint ?? null, ownershipIdentity: row.ownershipIdentity, legacy: false,
286
286
  })),
287
- foreignRows: rows.filter((row: any) => row.harness !== HARNESS).map((row: any) => ({ harness: row.harness, identity: row.identity, target: row.target })),
287
+ foreignRows: rows.filter((row ) => row.harness !== HARNESS).map((row ) => ({ harness: row.harness, identity: row.identity, target: row.target })),
288
288
  identityFor: ownershipIdentity, assetId: pseudoAssetId(assets), renderer: MCP_RENDERER, label: 'OpenCode MCP',
289
289
  };
290
290
  }
291
291
 
292
292
  /** opencode.json cannot be edited safely while removing: its registration row is released with
293
293
  * the entry rows, so the removal completes and leaves the file as it is. */
294
- function releasedConfigRow({ assets, paths, ledger }: any) {
295
- const owned = (ledger?.resources || []).some((row: any) => row.harness === HARNESS && row.ownershipIdentity === `${HARNESS}:config:registration` && row.target === paths.config);
294
+ function releasedConfigRow({ assets, paths, ledger } ) {
295
+ const owned = (ledger?.resources || []).some((row ) => row.harness === HARNESS && row.ownershipIdentity === `${HARNESS}:config:registration` && row.target === paths.config);
296
296
  return owned ? [{ assetId: pseudoAssetId(assets), target: paths.config, operation: 'remove', release: true,
297
297
  ownershipIdentity: `${HARNESS}:config:registration`, harness: HARNESS, projection: { renderer: 'opencode-config' } }] : [];
298
298
  }
299
299
 
300
- function plan({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, context = {}, ledger, fsImpl = fs }: any) {
300
+ function plan({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, context = {}, ledger, fsImpl = fs } ) {
301
301
  const found = discovery || discover({ scope, scopeRoot, mcp, mcpAdoptable, ledger, context, fsImpl });
302
302
  const changes = [];
303
303
  const conflicts = [];
@@ -306,7 +306,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
306
306
  // below, under a fixed reason that never quotes the file.
307
307
  const configReadable = found.mcpSnapshot[found.paths.config].ok;
308
308
 
309
- const guidance = assets.find((asset: any) => asset.capability === 'instructions');
309
+ const guidance = assets.find((asset ) => asset.capability === 'instructions');
310
310
  if (guidance) {
311
311
  const rendered = render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') });
312
312
  const outcome = removing
@@ -333,7 +333,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
333
333
  // Tagged 'remove' during a remove operation (rather than always 'update'/'create') so this
334
334
  // adapter's own apply()/remove() dispatch — which routes strictly on `operation` — writes
335
335
  // the unmerged file during removal instead of silently skipping it (the same fix
336
- // src/adapters/copilot/index.ts's analogous mcp-merge change already carries).
336
+ // src/adapters/copilot/index.js's analogous mcp-merge change already carries).
337
337
  operation: removing ? 'remove' : (found.config.exists ? 'update' : 'create'), content: `${JSON.stringify(next, null, 2)}\n`,
338
338
  ownershipIdentity: `${HARNESS}:config:registration`, fingerprint: fingerprint(next),
339
339
  harness: HARNESS, projection: { renderer: 'opencode-config' } });
@@ -355,14 +355,14 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
355
355
  return { changes, conflicts, notices: [...copyTree.notices, ...mcpPlan.notices], paths: found.paths };
356
356
  }
357
357
 
358
- function writeChange(change: any, fsImpl: any) {
358
+ function writeChange(change , fsImpl ) {
359
359
  fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
360
360
  fsImpl.writeFileSync(change.target, change.content, 'utf8');
361
361
  }
362
362
 
363
363
  // The config change is written first; the entry changes then go to a fresh read of the same file,
364
364
  // so they apply on top of it.
365
- function apply({ changes = [], fsImpl = fs }: any) {
365
+ function apply({ changes = [], fsImpl = fs } ) {
366
366
  let applied = 0;
367
367
  for (const change of changes) {
368
368
  if (change.operation === 'remove') continue;
@@ -378,7 +378,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
378
378
  return { applied };
379
379
  }
380
380
 
381
- function remove({ changes = [], fsImpl = fs }: any) {
381
+ function remove({ changes = [], fsImpl = fs } ) {
382
382
  let removed = 0;
383
383
  for (const change of changes) {
384
384
  if (change.operation !== 'remove') continue;
@@ -392,13 +392,13 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
392
392
  return { removed };
393
393
  }
394
394
 
395
- function verify({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, ledger, operation, context = {}, fsImpl = fs }: any) {
395
+ function verify({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, ledger, operation, context = {}, fsImpl = fs } ) {
396
396
  const found = discover({ scope, scopeRoot, mcp, mcpAdoptable, ledger, context, fsImpl });
397
397
  const removing = (operation ?? context.operation) === 'remove';
398
398
  const statuses = [];
399
399
  const resources = [];
400
400
  const conflicts = [];
401
- const instructionsAssetId = assets.find((asset: any) => asset.capability === 'instructions')?.id ?? pseudoAssetId(assets);
401
+ const instructionsAssetId = assets.find((asset ) => asset.capability === 'instructions')?.id ?? pseudoAssetId(assets);
402
402
 
403
403
  const hasSection = typeof found.instruction === 'string'
404
404
  && found.instruction.includes(MARKER_START) && found.instruction.includes(MARKER_END);
@@ -445,7 +445,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
445
445
 
446
446
  const copyTree = verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl });
447
447
  resources.push(...copyTree.resources);
448
- statuses.push(...copyTree.statuses.map((status: any) => ({ harness: HARNESS, ...status })));
448
+ statuses.push(...copyTree.statuses.map((status ) => ({ harness: HARNESS, ...status })));
449
449
  conflicts.push(...copyTree.conflicts);
450
450
 
451
451
  return { ok: conflicts.length === 0 && !statuses.some((s) => s.status === 'invalid' || s.status === 'conflict'), resources, statuses, conflicts };
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { fingerprint } = require('../copy-tree.js');
8
+
9
+ const ASSET_ID = 'pi.guard-extension';
10
+ const EXTENSION_NAME = 'doflow-guard.js';
11
+ const OWNERSHIP = 'pi:guard-extension';
12
+ const TEMPLATE = 'core/harnesses/pi/extensions/doflow-guard.js';
13
+ const BRIDGE_SUFFIX = ['.doflow', 'shared', 'hooks', 'bridge', 'bridge.sh'];
14
+
15
+ function userAgentDir(scopeRoot , env ) {
16
+ const override = env?.PI_CODING_AGENT_DIR;
17
+ return typeof override === 'string' && override.trim() ? path.resolve(override.trim()) : path.join(scopeRoot, '.pi', 'agent');
18
+ }
19
+
20
+ function targetPath(scopeRoot , env , scope = 'user') {
21
+ return scope === 'project'
22
+ ? path.join(scopeRoot, '.pi', 'extensions', EXTENSION_NAME)
23
+ : path.join(userAgentDir(scopeRoot, env), 'extensions', EXTENSION_NAME);
24
+ }
25
+
26
+ function renderExtension({ repoRoot, scopeRoot, target, fsImpl = fs } ) {
27
+ const templatePath = path.join(repoRoot, TEMPLATE);
28
+ const template = fsImpl.readFileSync(templatePath, 'utf8');
29
+ const bridgePath = path.join(scopeRoot, ...BRIDGE_SUFFIX);
30
+ const relative = path.relative(path.dirname(target), bridgePath).split(path.sep).join('/');
31
+ const rendered = template.replace("const BRIDGE = new URL('../../shared/hooks/bridge/bridge.sh', import.meta.url);",
32
+ `const BRIDGE = new URL(${JSON.stringify(relative)}, import.meta.url);`);
33
+ if (rendered === template) throw new Error('Pi extension bridge path template was not found');
34
+ return rendered;
35
+ }
36
+
37
+ function planPiHooks({ scope, scopeRoot, assets = [], ledger, context = {}, adopt = false, env = process.env, fsImpl = fs } ) {
38
+ if (scope !== 'user' && scope !== 'global' && scope !== 'project') return { changes: [], conflicts: [], notices: [] };
39
+ const asset = assets.find((row ) => row.id === ASSET_ID);
40
+ if (!asset) return { changes: [], conflicts: [], notices: [] };
41
+ const computedTarget = targetPath(scopeRoot, env, scope);
42
+ const removing = context.operation === 'remove';
43
+ const ownedRows = (ledger?.resources || []).filter((row ) => row.harness === 'pi' && row.assetId === ASSET_ID
44
+ && row.ownershipIdentity === OWNERSHIP);
45
+ const exactOwned = ownedRows.find((row ) => row.target === computedTarget);
46
+ const owned = exactOwned || (removing && ownedRows.length === 1 ? ownedRows[0] : undefined);
47
+ const target = removing && owned ? owned.target : computedTarget;
48
+ const exists = fsImpl.existsSync(target);
49
+ const notices = [];
50
+ if (removing) {
51
+ if (!owned) return { changes: [], conflicts: [], notices };
52
+ if (!exists) return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', release: true,
53
+ ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
54
+ const current = fsImpl.readFileSync(target, 'utf8');
55
+ if (fingerprint(current) !== owned.fingerprint) {
56
+ notices.push(`kept hand-edited ${target}; DoFlow no longer manages it`);
57
+ return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', release: true,
58
+ ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
59
+ }
60
+ return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', content: null,
61
+ ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
62
+ }
63
+
64
+ const content = renderExtension({ repoRoot: context.repoRoot, scopeRoot, target, fsImpl });
65
+ if (!exists) return { changes: [{ assetId: ASSET_ID, target, operation: 'create', content,
66
+ ownershipIdentity: OWNERSHIP, harness: 'pi', fingerprint: fingerprint(content),
67
+ projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
68
+ if (!owned && !adopt) {
69
+ return { changes: [], conflicts: [`Pi extension already exists and is not owned by DoFlow: ${target}`], notices };
70
+ }
71
+ const current = fsImpl.readFileSync(target, 'utf8');
72
+ if (owned && fingerprint(current) !== owned.fingerprint) {
73
+ return { changes: [], conflicts: [`Pi extension was modified outside DoFlow: ${target}`], notices };
74
+ }
75
+ if (current === content) return { changes: [], conflicts: [], notices };
76
+ return { changes: [{ assetId: ASSET_ID, target, operation: 'update', content,
77
+ ownershipIdentity: OWNERSHIP, harness: 'pi', fingerprint: fingerprint(content),
78
+ projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
79
+ }
80
+
81
+ function verifyPiHooks({ scope, scopeRoot, assets = [], ledger, context = {}, adopt = false, env = process.env, fsImpl = fs, pathEnv = process.env.PATH } ) {
82
+ if (scope !== 'user' && scope !== 'global' && scope !== 'project') return { statuses: [], resources: [], conflicts: [] };
83
+ const asset = assets.find((row ) => row.id === ASSET_ID);
84
+ if (!asset) return { statuses: [], resources: [], conflicts: [] };
85
+ const target = targetPath(scopeRoot, env, scope);
86
+ const owned = (ledger?.resources || []).find((row ) => row.harness === 'pi' && row.assetId === ASSET_ID
87
+ && row.ownershipIdentity === OWNERSHIP && row.target === target);
88
+ const bridgePath = path.join(scopeRoot, ...BRIDGE_SUFFIX);
89
+ const present = fsImpl.existsSync(target);
90
+ const content = present ? fsImpl.readFileSync(target, 'utf8') : null;
91
+ const actualFingerprint = content === null ? null : fingerprint(content);
92
+ const fingerprintMatches = !!owned && actualFingerprint === owned.fingerprint;
93
+ const plannedWrite = (context?.plannedWrites || []).includes(target);
94
+ const bridgeMatches = !!content && content.includes(JSON.stringify(path.relative(path.dirname(target), bridgePath).split(path.sep).join('/')));
95
+ const commandOnPath = (command ) => String(pathEnv || '').split(path.delimiter)
96
+ .some((dir) => fsImpl.existsSync(path.join(dir, command)));
97
+ const status = present && !owned && !plannedWrite && !adopt ? 'conflict'
98
+ : (present && (fingerprintMatches || plannedWrite) && bridgeMatches && commandOnPath('bash') && commandOnPath('jq') ? 'managed' : 'absent');
99
+ const resources = status === 'managed' ? [{ assetId: ASSET_ID, target, ownershipIdentity: OWNERSHIP,
100
+ fingerprint: actualFingerprint, sourceVersion: context.sourceVersion ?? 'unknown',
101
+ projection: { renderer: 'pi-guard-extension' } }] : [];
102
+ const statuses = [{ harness: 'pi', assetId: ASSET_ID, capability: 'hooks', status, target,
103
+ ownershipIdentity: OWNERSHIP, fingerprint: actualFingerprint, fingerprintMatches,
104
+ bridgePath, bridgeExists: fsImpl.existsSync(bridgePath), bash: commandOnPath('bash'), jq: commandOnPath('jq'),
105
+ prerequisites: scope === 'project' ? ['project-trust'] : [], piCodingAgentDir: typeof env?.PI_CODING_AGENT_DIR === 'string' && env.PI_CODING_AGENT_DIR.trim() ? env.PI_CODING_AGENT_DIR.trim() : null }];
106
+ const conflicts = status === 'conflict' ? [`Pi extension is not owned by DoFlow: ${target}`] : [];
107
+ return { statuses, resources, conflicts };
108
+ }
109
+
110
+ function removePiHooks(changes , { fsImpl = fs } = {}) {
111
+ let removed = 0;
112
+ for (const change of changes) {
113
+ if (change.projection?.renderer !== 'pi-guard-extension' || change.operation !== 'remove' || change.release) continue;
114
+ if (fsImpl.existsSync(change.target)) { fsImpl.unlinkSync(change.target); removed += 1; }
115
+ }
116
+ return removed;
117
+ }
118
+
119
+ module.exports = { ASSET_ID, EXTENSION_NAME, OWNERSHIP, userAgentDir, targetPath, renderExtension, planPiHooks, verifyPiHooks, removePiHooks };