@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
@@ -13,9 +13,7 @@
13
13
  //
14
14
  // MCP servers are merged per server into Pi's mcp.json (.pi/mcp.json project; ~/.pi/agent/mcp.json
15
15
  // or $PI_CODING_AGENT_DIR/mcp.json user) by ./mcp.ts; Pi reads it through its built-in MCP from
16
- // 0.99.0. Hooks are absent — Pi lifecycle handlers are TypeScript modules registered via pi.on(),
17
- // executing with full system permissions, which is a different trust model from the shell scripts
18
- // DoFlow ships.
16
+ // 0.99.0. The user- and project-scope lifecycle extension is ledger-owned and resolves its shared bridge at install time.
19
17
  //
20
18
  // Evidence: https://pi.dev/docs/latest/settings, https://pi.dev/docs/latest/skills,
21
19
  // https://pi.dev/docs/latest/quickstart, https://pi.dev/docs/latest/extensions,
@@ -23,13 +21,15 @@
23
21
  const fs = require('node:fs');
24
22
  const path = require('node:path');
25
23
 
26
- const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.ts');
27
- const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor } = require('../copy-tree.ts');
28
- const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.ts');
29
- const { mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp } = require('./mcp.ts');
24
+ const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
25
+ const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor } = require('../copy-tree.js');
26
+ const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
27
+ const { mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp } = require('./mcp.js');
28
+ const { planPiHooks, verifyPiHooks, removePiHooks } = require('./hooks.js');
30
29
 
31
30
  const HARNESS = 'pi';
32
- const AGENT_DIR_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, but DoFlow moves only mcp.json there; skills and AGENTS.md stay in ~/.pi/agent.';
31
+ const AGENT_DIR_MCP_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, but DoFlow moves only mcp.json there; skills and AGENTS.md stay in ~/.pi/agent.';
32
+ const AGENT_DIR_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, and DoFlow moves mcp.json and the guard extension there; skills and AGENTS.md stay in ~/.pi/agent.';
33
33
 
34
34
  // Native path facts live in core/registry/harnesses.json under this harness's "paths" section and
35
35
  // resolve through the shared harness-paths resolver; nothing below hardcodes them.
@@ -40,11 +40,11 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
40
40
  // Parity note: Pi's historical nativePaths() derived even its user-scope root from scopeRoot
41
41
  // (the CLI passes $HOME there for -g installs), never from a separate homeDir argument — so the
42
42
  // resolver is fed no homeDir and "root" keeps resolving exactly as before.
43
- function nativePaths({ scope, scopeRoot }: any = {}) {
43
+ function nativePaths({ scope, scopeRoot } = {}) {
44
44
  const paths = resolveHarnessPaths(declaredPaths, { scope, scopeRoot });
45
45
  // Pi's user-scope mcp.json follows $PI_CODING_AGENT_DIR when set. It is not declared in the
46
- // registry because the path schema has no environment base (src/helper/harness-paths.js); only
47
- // mcp moves, skills and AGENTS.md keep their declared locations.
46
+ // registry because the path schema has no environment base (src/helper/harness-paths.js); the
47
+ // guard extension follows it too, while skills and AGENTS.md keep their declared locations.
48
48
  if ((scope === 'user' || scope === 'global') && agentDirOverride()) paths.mcp = path.join(path.resolve(agentDirOverride()), 'mcp.json');
49
49
  return paths;
50
50
  }
@@ -54,7 +54,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
54
54
  return typeof agentDir === 'string' && agentDir.trim() !== '' ? agentDir.trim() : null;
55
55
  }
56
56
 
57
- function discover({ scope, scopeRoot, mcp = [], ledger, context = {}, fsImpl = fs }: any) {
57
+ function discover({ scope, scopeRoot, mcp = [], ledger, context = {}, fsImpl = fs } ) {
58
58
  const paths = nativePaths({ scope, scopeRoot });
59
59
  const instruction = fsImpl.existsSync(paths.instruction) ? fsImpl.readFileSync(paths.instruction, 'utf8') : null;
60
60
  const rows = mcpRows(ledger);
@@ -62,18 +62,18 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
62
62
  // Pi reads mcp.json only for a selection or a row; the names in it are read here regardless.
63
63
  const file = snapshot[paths.mcp] ?? discoverPiMcp({ selected: [true], rows: [], file: paths.mcp, fsImpl })[paths.mcp];
64
64
  const members = file?.exists && file.doc?.ok ? file.doc.servers?.members ?? [] : null;
65
- const mcpConfigured = members && members.map((member: any) => member.key).sort();
65
+ const mcpConfigured = members && members.map((member ) => member.key).sort();
66
66
  return { paths, instruction, mcp: snapshot, mcpOwned: ownedPiMcpIds({ rows, snapshot }), mcpConfigured };
67
67
  }
68
68
 
69
- function render({ content = '' }: any = {}) {
69
+ function render({ content = '' } = {}) {
70
70
  return `${MARKER_START}\n${String(content).trimEnd()}\n${MARKER_END}\n`;
71
71
  }
72
72
 
73
73
  /** Replace only the span between DoFlow's markers. An AGENTS.md with no DoFlow section is refused
74
74
  * rather than appended to — Pi shares AGENTS.md with every other agent that reads it, so where
75
75
  * DoFlow's content belongs is its owner's decision. Add an empty marker pair to opt in. */
76
- function managedInstruction(existing: any, rendered: any) {
76
+ function managedInstruction(existing , rendered ) {
77
77
  if (existing === null) return { ok: true, operation: 'create', content: rendered };
78
78
  const start = existing.indexOf(MARKER_START);
79
79
  const end = existing.indexOf(MARKER_END);
@@ -83,7 +83,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
83
83
  return { ok: true, operation: content === existing ? 'none' : 'merge', content };
84
84
  }
85
85
 
86
- function strippedInstruction(existing: any) {
86
+ function strippedInstruction(existing ) {
87
87
  if (typeof existing !== 'string') return null;
88
88
  const start = existing.indexOf(MARKER_START);
89
89
  const end = existing.indexOf(MARKER_END);
@@ -95,7 +95,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
95
95
 
96
96
  // ---- copy-tree assets (skills) ----
97
97
 
98
- function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs }: any) {
98
+ function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs } ) {
99
99
  const paths = nativePaths({ scope, scopeRoot });
100
100
  const changes = [];
101
101
  const conflicts = [];
@@ -112,10 +112,10 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
112
112
  // is never deleted on removal, forced or not.
113
113
  force: !removing && context?.force === true, });
114
114
  treeResults.push(result);
115
- conflicts.push(...result.conflicts.map((reason: any) => `${asset.id}: ${reason}`));
115
+ conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
116
116
  // A hand-edited file at a path DoFlow no longer writes is the user's: it stays, only its
117
117
  // ownership row is released.
118
- notices.push(...result.kept.map((item: any) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
118
+ notices.push(...result.kept.map((item ) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
119
119
  for (const change of result.changes) {
120
120
  changes.push({
121
121
  assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
@@ -130,19 +130,19 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
130
130
  return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
131
131
  }
132
132
 
133
- function applyCopyTreeAssets(changes: any, { fsImpl = fs }: any = {}) {
134
- const treeChanges = changes.filter((change: any) => change.projection?.renderer === 'copy-tree' && change.operation !== 'remove')
135
- .map((change: any) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint }));
133
+ function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
134
+ const treeChanges = changes.filter((change ) => change.projection?.renderer === 'copy-tree' && change.operation !== 'remove')
135
+ .map((change ) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint }));
136
136
  return applyTree({ changes: treeChanges, fsImpl }).applied;
137
137
  }
138
138
 
139
- function removeCopyTreeAssets(changes: any, { fsImpl = fs }: any = {}) {
140
- const treeChanges = changes.filter((change: any) => change.kind === 'copy-tree-file' && change.operation === 'remove' && !change.retained)
141
- .map((change: any) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
139
+ function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
140
+ const treeChanges = changes.filter((change ) => change.kind === 'copy-tree-file' && change.operation === 'remove' && !change.retained)
141
+ .map((change ) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
142
142
  return removeTree({ changes: treeChanges, fsImpl }).removed;
143
143
  }
144
144
 
145
- function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs }: any) {
145
+ function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs } ) {
146
146
  const paths = nativePaths({ scope, scopeRoot });
147
147
  const statuses = [];
148
148
  const resources = [];
@@ -151,7 +151,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
151
151
  const destDir = sharedTreeDestDir(paths.root, asset.nativeDir) ?? copyTreeDestDir(paths.configDir, asset);
152
152
  const sourceDir = sourceDirFor(asset, context, fsImpl, 'Pi');
153
153
  const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, plannedWrites: context?.plannedWrites });
154
- conflicts.push(...result.conflicts.map((reason: any) => `${asset.id}: ${reason}`));
154
+ conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
155
155
  for (const resource of result.resources) {
156
156
  resources.push({
157
157
  assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:pi:copy-tree:${asset.id}:${resource.relPath}`,
@@ -167,13 +167,13 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
167
167
 
168
168
  // ---- shared adapter contract ----
169
169
 
170
- function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs }: any) {
170
+ function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs } ) {
171
171
  const found = discovery || discover({ scope, scopeRoot, mcp, ledger, context, fsImpl });
172
172
  const changes = [];
173
173
  const conflicts = [];
174
174
  const removing = context.operation === 'remove';
175
175
 
176
- const guidance = assets.find((asset: any) => asset.capability === 'instructions');
176
+ const guidance = assets.find((asset ) => asset.capability === 'instructions');
177
177
  if (guidance) {
178
178
  const rendered = render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') });
179
179
  const outcome = removing
@@ -191,23 +191,40 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
191
191
  changes.push(...copyTree.changes);
192
192
  conflicts.push(...copyTree.conflicts);
193
193
 
194
+ const piHooks = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
195
+ changes.push(...piHooks.changes);
196
+ conflicts.push(...piHooks.conflicts);
197
+
194
198
  const piMcp = planPiMcp({ selected: mcp, rows: mcpRows(ledger), file: found.paths.mcp, scope, removing, snapshot: found.mcp, assets });
195
199
  changes.push(...piMcp.changes);
196
200
  conflicts.push(...piMcp.conflicts);
197
201
 
198
- const notices = [...copyTree.notices, ...piMcp.notices];
199
- if (!removing && scope !== 'project' && agentDirOverride()) notices.push(AGENT_DIR_NOTICE);
202
+ const notices = [...copyTree.notices, ...piMcp.notices, ...piHooks.notices];
203
+ if (!removing && scope !== 'project' && agentDirOverride()) {
204
+ notices.push(assets.some((asset ) => asset.id === 'pi.guard-extension') ? AGENT_DIR_NOTICE : AGENT_DIR_MCP_NOTICE);
205
+ }
200
206
  return { changes, conflicts, notices, paths: found.paths };
201
207
  }
202
208
 
203
- function writeChange(change: any, fsImpl: any) {
209
+ /** The hook front door (IC-037): the guard extension. */
210
+ function planHookDoors({ scope, scopeRoot, assets = [], context = {}, ledger, fsImpl = fs } ) {
211
+ const door = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
212
+ return { changes: door.changes, conflicts: door.conflicts, notices: door.notices };
213
+ }
214
+
215
+ function verifyHookDoors({ scope, scopeRoot, assets = [], ledger, context = {}, fsImpl = fs } ) {
216
+ const door = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
217
+ return { ok: door.conflicts.length === 0 && !door.statuses.some((s ) => s.status === 'invalid' || s.status === 'conflict'), ...door };
218
+ }
219
+
220
+ function writeChange(change , fsImpl ) {
204
221
  fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
205
222
  fsImpl.writeFileSync(change.target, change.content, 'utf8');
206
223
  }
207
224
 
208
- function isPiMcp(change: any) { return change.projection?.renderer === 'pi-mcp'; }
225
+ function isPiMcp(change ) { return change.projection?.renderer === 'pi-mcp'; }
209
226
 
210
- function apply({ changes = [], fsImpl = fs }: any) {
227
+ function apply({ changes = [], fsImpl = fs } ) {
211
228
  // MCP first: its base-hash check refuses a stale plan before any other Pi file is written.
212
229
  let applied = applyPiMcp(changes.filter(isPiMcp), { fsImpl }).applied;
213
230
  for (const change of changes) {
@@ -222,24 +239,26 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
222
239
  return { applied };
223
240
  }
224
241
 
225
- function remove({ changes = [], fsImpl = fs }: any) {
242
+ function remove({ changes = [], fsImpl = fs } ) {
226
243
  let removed = applyPiMcp(changes.filter(isPiMcp), { fsImpl }).applied;
227
244
  for (const change of changes) {
228
245
  if (change.operation !== 'remove' || change.projection?.renderer === 'copy-tree' || isPiMcp(change)) continue;
246
+ if (change.projection?.renderer === 'pi-guard-extension') continue;
229
247
  if (change.content === null) continue;
230
248
  writeChange(change, fsImpl);
231
249
  removed += 1;
232
250
  }
251
+ removed += removePiHooks(changes, { fsImpl });
233
252
  removed += removeCopyTreeAssets(changes, { fsImpl });
234
253
  return { removed };
235
254
  }
236
255
 
237
- function verify({ scope, scopeRoot, assets = [], mcp = [], discovery, ledger, context = {}, fsImpl = fs }: any) {
256
+ function verify({ scope, scopeRoot, assets = [], mcp = [], discovery, ledger, context = {}, fsImpl = fs } ) {
238
257
  const found = discover({ scope, scopeRoot, mcp, ledger, context, fsImpl });
239
258
  const statuses = [];
240
259
  const resources = [];
241
260
  const conflicts = [];
242
- const instructionsAssetId = assets.find((asset: any) => asset.capability === 'instructions')?.id ?? assets[0]?.id;
261
+ const instructionsAssetId = assets.find((asset ) => asset.capability === 'instructions')?.id ?? assets[0]?.id;
243
262
 
244
263
  const hasSection = typeof found.instruction === 'string'
245
264
  && found.instruction.includes(MARKER_START) && found.instruction.includes(MARKER_END);
@@ -263,10 +282,15 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
263
282
  resources.push(...piMcp.resources);
264
283
  conflicts.push(...piMcp.conflicts);
265
284
 
285
+ const piHooks = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
286
+ statuses.push(...piHooks.statuses);
287
+ resources.push(...piHooks.resources);
288
+ conflicts.push(...piHooks.conflicts);
289
+
266
290
  return { ok: conflicts.length === 0 && !statuses.some((s) => s.status === 'invalid' || s.status === 'conflict'), resources, statuses, conflicts };
267
291
  }
268
292
 
269
- return { nativePaths, discover, render, plan, apply, remove, verify };
293
+ return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
270
294
  }
271
295
 
272
296
  const singleton = createPiAdapter();
@@ -274,6 +298,7 @@ const singleton = createPiAdapter();
274
298
  module.exports = {
275
299
  HARNESS, nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
276
300
  plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
277
- managedInstruction: (singleton as any).managedInstruction, strippedInstruction: (singleton as any).strippedInstruction,
301
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
302
+ managedInstruction: (singleton ).managedInstruction, strippedInstruction: (singleton ).strippedInstruction,
278
303
  createPiAdapter,
279
304
  };
@@ -10,17 +10,17 @@
10
10
  const SKELETON = '{\n "mcpServers": {}\n}\n';
11
11
  const WHITESPACE = ' \t\n\r';
12
12
 
13
- function isPlainObject(value: any): boolean {
13
+ function isPlainObject(value ) {
14
14
  return value !== null && typeof value === 'object' && !Array.isArray(value);
15
15
  }
16
16
 
17
- function skipWhitespace(text: string, i: number): number {
17
+ function skipWhitespace(text , i ) {
18
18
  while (i < text.length && WHITESPACE.includes(text[i])) i += 1;
19
19
  return i;
20
20
  }
21
21
 
22
22
  /** Index just past the string token that opens at `i`. */
23
- function stringEnd(text: string, i: number): number {
23
+ function stringEnd(text , i ) {
24
24
  for (i += 1; text[i] !== '"'; i += 1) if (text[i] === '\\') i += 1;
25
25
  return i + 1;
26
26
  }
@@ -28,7 +28,7 @@ function stringEnd(text: string, i: number): number {
28
28
  // The text has already passed JSON.parse, so the scanner only has to find spans, never validate.
29
29
  // Only the top-level object and mcpServers need member spans; every other value is skipped by
30
30
  // counting brackets, without recursion, so a deeply nested file cannot exhaust the stack.
31
- function valueEnd(text: string, i: number): number {
31
+ function valueEnd(text , i ) {
32
32
  const ch = text[i];
33
33
  if (ch === '"') return stringEnd(text, i);
34
34
  if (ch !== '{' && ch !== '[') {
@@ -47,8 +47,8 @@ function valueEnd(text: string, i: number): number {
47
47
 
48
48
  /** Member spans of the object opening at `open`; `nested` names the one member whose object value
49
49
  * is scanned for its own members too. */
50
- function scanObject(text: string, open: number, nested: string | null = null): { open: number, close: number, members: any[], [extra: string]: any } {
51
- const members: any[] = [];
50
+ function scanObject(text , open , nested = null) {
51
+ const members = [];
52
52
  let i = skipWhitespace(text, open + 1);
53
53
  while (text[i] !== '}') {
54
54
  const keyStart = i;
@@ -66,7 +66,7 @@ function scanObject(text: string, open: number, nested: string | null = null): {
66
66
 
67
67
  /** JSON.parse's message quotes the text around the error, which may be a token or a password, so
68
68
  * only the position is reported. */
69
- function invalidJsonReason(text: string, error: any): string {
69
+ function invalidJsonReason(text , error ) {
70
70
  const match = /at position (\d+)/.exec(error.message);
71
71
  if (!match) return 'invalid JSON';
72
72
  const before = text.slice(0, Number(match[1]));
@@ -75,14 +75,14 @@ function invalidJsonReason(text: string, error: any): string {
75
75
  }
76
76
 
77
77
  /** The whitespace that starts the line `at` sits on, or null when something else precedes it there. */
78
- function lineIndent(text: string, at: number): string | null {
78
+ function lineIndent(text , at ) {
79
79
  const prefix = text.slice(text.lastIndexOf('\n', at - 1) + 1, at);
80
80
  return /^[ \t]*$/.test(prefix) ? prefix : null;
81
81
  }
82
82
 
83
83
  /** The line ending most of the lines in `[start, end)` use, or `fallback` when the span has none:
84
84
  * one stray CRLF in an LF file does not turn every inserted line into CRLF. */
85
- function lineEnding(text: string, start: number, end: number, fallback: string): string {
85
+ function lineEnding(text , start , end , fallback ) {
86
86
  const span = text.slice(start, end);
87
87
  const lines = span.split('\n').length - 1;
88
88
  if (!lines) return fallback;
@@ -91,7 +91,7 @@ function lineEnding(text: string, start: number, end: number, fallback: string):
91
91
 
92
92
  /** Attach the formatting a container's new members follow: whether it spans lines, its line ending,
93
93
  * the indent of its members and the indent of its closing brace. An empty container takes its parent's. */
94
- function attachLayout(text: string, node: any, parent: any, unit: string, eol: string): void {
94
+ function attachLayout(text , node , parent , unit , eol ) {
95
95
  const parentIndent = parent ? parent.indent : '';
96
96
  const empty = node.members.length === 0;
97
97
  node.multiline = text.slice(node.open, node.close).includes('\n') || (empty && Boolean(parent?.multiline));
@@ -100,8 +100,8 @@ function attachLayout(text: string, node: any, parent: any, unit: string, eol: s
100
100
  node.closeIndent = parentIndent;
101
101
  }
102
102
 
103
- function duplicateKey(node: any): string | null {
104
- const seen = new Set<string>();
103
+ function duplicateKey(node ) {
104
+ const seen = new Set ();
105
105
  for (const member of node.members) {
106
106
  if (seen.has(member.key)) return member.key;
107
107
  seen.add(member.key);
@@ -109,9 +109,9 @@ function duplicateKey(node: any): string | null {
109
109
  return null;
110
110
  }
111
111
 
112
- function readDocument(text: string): any {
112
+ function readDocument(text ) {
113
113
  let parsed;
114
- try { parsed = JSON.parse(text); } catch (error: any) { return { ok: false, reason: invalidJsonReason(text, error) }; }
114
+ try { parsed = JSON.parse(text); } catch (error ) { return { ok: false, reason: invalidJsonReason(text, error) }; }
115
115
  if (!isPlainObject(parsed)) return { ok: false, reason: 'top level is not an object' };
116
116
  if (Object.prototype.hasOwnProperty.call(parsed, 'mcpServers') && !isPlainObject(parsed.mcpServers)) {
117
117
  return { ok: false, reason: 'mcpServers is not an object' };
@@ -120,7 +120,7 @@ function readDocument(text: string): any {
120
120
  try { root = scanObject(text, skipWhitespace(text, 0), 'mcpServers'); } catch { return { ok: false, reason: 'the file could not be scanned' }; }
121
121
  const duplicateTop = duplicateKey(root);
122
122
  if (duplicateTop !== null) return { ok: false, reason: `duplicate key '${duplicateTop}'` };
123
- const servers = root.members.find((member: any) => member.key === 'mcpServers')?.node ?? null;
123
+ const servers = root.members.find((member ) => member.key === 'mcpServers')?.node ?? null;
124
124
  const duplicateServer = servers ? duplicateKey(servers) : null;
125
125
  if (duplicateServer !== null) return { ok: false, reason: `duplicate key '${duplicateServer}'` };
126
126
 
@@ -132,21 +132,21 @@ function readDocument(text: string): any {
132
132
  return { ok: true, root, servers, eol, unit };
133
133
  }
134
134
 
135
- function renderValue(value: any, container: any, layout: any): string {
135
+ function renderValue(value , container , layout ) {
136
136
  if (!container.multiline) return JSON.stringify(value);
137
137
  return JSON.stringify(value, null, layout.unit).split('\n').join(container.eol + container.indent);
138
138
  }
139
139
 
140
- function renderMember(key: string, value: any, container: any, layout: any): string {
140
+ function renderMember(key , value , container , layout ) {
141
141
  return `${JSON.stringify(key)}: ${renderValue(value, container, layout)}`;
142
142
  }
143
143
 
144
- function splice(text: string, start: number, end: number, inserted: string): string {
144
+ function splice(text , start , end , inserted ) {
145
145
  return text.slice(0, start) + inserted + text.slice(end);
146
146
  }
147
147
 
148
148
  /** `layout` is the readDocument result the container or member came from. */
149
- function insertMember(text: string, container: any, key: string, value: any, layout: any): string {
149
+ function insertMember(text , container , key , value , layout ) {
150
150
  const member = renderMember(key, value, container, layout);
151
151
  const last = container.members[container.members.length - 1];
152
152
  if (last) {
@@ -159,12 +159,12 @@ function insertMember(text: string, container: any, key: string, value: any, lay
159
159
  return splice(text, container.open + 1, container.close, inserted);
160
160
  }
161
161
 
162
- function replaceValue(text: string, member: any, value: any, layout: any): string {
162
+ function replaceValue(text , member , value , layout ) {
163
163
  const container = layout.servers?.members.includes(member) ? layout.servers : layout.root;
164
164
  return splice(text, member.valueStart, member.valueEnd, renderValue(value, container, layout));
165
165
  }
166
166
 
167
- function removeMember(text: string, container: any, member: any): string {
167
+ function removeMember(text , container , member ) {
168
168
  const index = container.members.indexOf(member);
169
169
  if (index === -1) throw new Error(`member '${member.key}' is not in this container`);
170
170
  if (container.members.length === 1) return splice(text, container.open + 1, container.close, '');
@@ -173,12 +173,12 @@ function removeMember(text: string, container: any, member: any): string {
173
173
  }
174
174
 
175
175
  /** Set the text between an empty container's braces, which a removal of its only member drops. */
176
- function replaceInner(text: string, container: any, inner: string): string {
176
+ function replaceInner(text , container , inner ) {
177
177
  if (container.members.length) throw new Error('only an empty container\'s inner text can be replaced');
178
178
  return splice(text, container.open + 1, container.close, inner);
179
179
  }
180
180
 
181
- function renderNewDocument(entries: any): string {
181
+ function renderNewDocument(entries ) {
182
182
  return `${JSON.stringify({ mcpServers: Object.fromEntries(entries) }, null, 2)}\n`;
183
183
  }
184
184