@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
@@ -8,12 +8,12 @@
8
8
  const crypto = require('node:crypto');
9
9
  const fs = require('node:fs');
10
10
  const path = require('node:path');
11
- const { parseToml, stripComment, tableViolations } = require('../../helper/toml.ts');
11
+ const { parseToml, stripComment, tableViolations } = require('../../helper/toml.js');
12
12
 
13
13
  const CONFIG_NAME = 'config.toml';
14
14
  const CONFIG_KIND = 'configuration-entry';
15
15
 
16
- function configPath({ scope, codexDir, projectRoot, paths }: { scope?: string, codexDir?: string, projectRoot?: string, paths?: any } = {}): string {
16
+ function configPath({ scope, codexDir, projectRoot, paths } = {}) {
17
17
  // Preferred: the declared path resolved by the adapter (harnesses.json "paths".configFile).
18
18
  if (paths?.configFile) return paths.configFile;
19
19
  // Legacy derivation for direct callers passing raw scope inputs (tests, reconcileCodexConfig);
@@ -29,23 +29,23 @@ function configPath({ scope, codexDir, projectRoot, paths }: { scope?: string, c
29
29
  throw new Error(`Unsupported Codex configuration scope: ${scope}`);
30
30
  }
31
31
 
32
- function fingerprint(value: any): string {
32
+ function fingerprint(value ) {
33
33
  return `sha256:${crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
34
34
  }
35
35
 
36
- function commentSuffix(line: string): string {
36
+ function commentSuffix(line ) {
37
37
  const withoutComment = stripComment(line);
38
38
  return line.slice(withoutComment.length);
39
39
  }
40
40
 
41
- function renderValue(value: any): string {
41
+ function renderValue(value ) {
42
42
  if (typeof value === 'boolean' || typeof value === 'number') return String(value);
43
43
  if (typeof value === 'string') return JSON.stringify(value);
44
44
  throw new Error('Managed Codex config values must be boolean, number, or string');
45
45
  }
46
46
 
47
- function normaliseDesired(resources: any): Map<string, any> {
48
- const result = new Map<string, any>();
47
+ function normaliseDesired(resources ) {
48
+ const result = new Map ();
49
49
  for (const resource of resources || []) {
50
50
  if (!resource || (resource.kind && resource.kind !== CONFIG_KIND && resource.kind !== 'config-entry')) continue;
51
51
  const identity = resource.identity ?? resource.key;
@@ -62,49 +62,49 @@ function normaliseDesired(resources: any): Map<string, any> {
62
62
 
63
63
  /** The comment on a line DoFlow added to a table the user opened with no entry: it is how removal
64
64
  * knows to keep the user's header and restore the file's exact bytes. */
65
- function addedMarker(table: string): string { return ` # DoFlow added this to your [${table}] table`; }
65
+ function addedMarker(table ) { return ` # DoFlow added this to your [${table}] table`; }
66
66
 
67
67
  /** Where a new entry `<table>.<key>` goes in the parsed file: after the last entry of its table;
68
68
  * on the line right after the table's empty header, marked; as a root dotted line after the last
69
69
  * root dotted line of that table; refused (`valueEntry`) when the table or a parent of it is set as
70
70
  * a value, which TOML cannot extend; otherwise in a new table (no `at`). */
71
- function placeEntry(identity: string, value: any, parsed: any): any {
71
+ function placeEntry(identity , value , parsed ) {
72
72
  const parts = identity.split('.');
73
73
  const table = parts.slice(0, -1).join('.');
74
74
  const rendered = renderValue(value);
75
75
  const assignment = `${parts.at(-1)} = ${rendered}`;
76
- const entries: any[] = [...parsed.entries];
77
- const after = (items: any[]) => Math.max(...items.map(([, entry]: any) => entry.line)) + 1;
78
- const inTable = entries.filter(([, entry]: any) => entry.table === table);
76
+ const entries = [...parsed.entries];
77
+ const after = (items ) => Math.max(...items.map(([, entry] ) => entry.line)) + 1;
78
+ const inTable = entries.filter(([, entry] ) => entry.table === table);
79
79
  if (inTable.length) return { table, at: after(inTable), line: assignment };
80
- const header = parsed.headers.find((item: any) => item.table === table);
80
+ const header = parsed.headers.find((item ) => item.table === table);
81
81
  if (header) return { table, at: header.line + 1, line: `${assignment}${addedMarker(table)}` };
82
- const dotted = entries.filter(([key, entry]: any) => entry.table === '' && key.startsWith(`${table}.`));
82
+ const dotted = entries.filter(([key, entry] ) => entry.table === '' && key.startsWith(`${table}.`));
83
83
  if (dotted.length) return { table, at: after(dotted), line: `${identity} = ${rendered}` };
84
- const valueEntry = entries.find(([key]: any) => key === table || table.startsWith(`${key}.`));
84
+ const valueEntry = entries.find(([key] ) => key === table || table.startsWith(`${key}.`));
85
85
  if (valueEntry) return { table, valueEntry };
86
86
  return { table, line: assignment };
87
87
  }
88
88
 
89
89
  /** Whether a managed entry's line is one placeEntry put into the user's own table: marked, or
90
90
  * written as a dotted key outside its table. */
91
- function placedLine(identity: string, entry: any, lines: any[]): boolean {
91
+ function placedLine(identity , entry , lines ) {
92
92
  const table = identity.split('.').slice(0, -1).join('.');
93
93
  return entry.table !== table || lines[entry.line].trimEnd().endsWith(addedMarker(table));
94
94
  }
95
95
 
96
96
  /** A conflict when `content` breaks a TOML table rule that `original` did not already break, so a
97
97
  * file that was valid is never written invalid; null otherwise. */
98
- function newViolation({ file, original, content, identities }: { file: string, original: string, content: string, identities: string[] }): string | null {
99
- const refused = (reason: string) => `${file}: DoFlow cannot write ${identities.map((identity) => `'${identity}'`).join(', ')} without making the file invalid TOML (${reason}). Nothing was written.`;
98
+ function newViolation({ file, original, content, identities } ) {
99
+ const refused = (reason ) => `${file}: DoFlow cannot write ${identities.map((identity) => `'${identity}'`).join(', ')} without making the file invalid TOML (${reason}). Nothing was written.`;
100
100
  const before = tableViolations(original);
101
101
  let after;
102
- try { after = tableViolations(content); } catch (error: any) { return refused(error.message); }
103
- const added = after.find((item: any) => !before.some((known: any) => known.kind === item.kind && known.table === item.table));
102
+ try { after = tableViolations(content); } catch (error ) { return refused(error.message); }
103
+ const added = after.find((item ) => !before.some((known ) => known.kind === item.kind && known.table === item.table));
104
104
  return added ? refused(`${added.kind} '${added.table}' on line ${added.line}`) : null;
105
105
  }
106
106
 
107
- function isOwnedRecord(resource: any, scope: any): boolean {
107
+ function isOwnedRecord(resource , scope ) {
108
108
  return resource && resource.target === 'codex' && resource.scope === scope &&
109
109
  (resource.kind === CONFIG_KIND || resource.kind === 'config-entry') && typeof resource.identity === 'string';
110
110
  }
@@ -118,20 +118,20 @@ function isOwnedRecord(resource: any, scope: any): boolean {
118
118
  * entries written before the neutral ledger existed could never be re-adopted — the condition
119
119
  * blocking the install was the same condition an install had to run to clear.
120
120
  */
121
- function planCodexConfig({ file, scope, managedResources = [], desiredResources = [], adopt = false }: { file: string, scope?: any, managedResources?: any[], desiredResources?: any[], adopt?: boolean }): any {
121
+ function planCodexConfig({ file, scope, managedResources = [], desiredResources = [], adopt = false } ) {
122
122
  const original = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
123
123
  let parsed;
124
- try { parsed = parseToml(original); } catch (error: any) {
124
+ try { parsed = parseToml(original); } catch (error ) {
125
125
  return { ok: false, status: 'malformed', file, original, changes: [], conflicts: [error.message], managedResources };
126
126
  }
127
127
  let desired;
128
- try { desired = normaliseDesired(desiredResources); } catch (error: any) {
128
+ try { desired = normaliseDesired(desiredResources); } catch (error ) {
129
129
  return { ok: false, status: 'invalid-request', file, original, changes: [], conflicts: [error.message], managedResources };
130
130
  }
131
- const owned = new Map<string, any>(managedResources.filter((resource: any) => isOwnedRecord(resource, scope)).map((resource: any) => [resource.identity, resource]));
132
- const changes: any[] = [];
133
- const conflicts: string[] = [];
134
- const placements = new Map<string, any>();
131
+ const owned = new Map (managedResources.filter((resource ) => isOwnedRecord(resource, scope)).map((resource ) => [resource.identity, resource]));
132
+ const changes = [];
133
+ const conflicts = [];
134
+ const placements = new Map ();
135
135
  const all = new Set([...desired.keys(), ...owned.keys()]);
136
136
  for (const identity of all) {
137
137
  const entry = parsed.entries.get(identity);
@@ -164,10 +164,10 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
164
164
  }
165
165
  if (conflicts.length) return { ok: false, status: 'conflict', file, original, changes: [], conflicts, managedResources };
166
166
  const nextLines = [...parsed.lines];
167
- for (const change of changes.filter((change: any) => change.type === 'update')) {
167
+ for (const change of changes.filter((change ) => change.type === 'update')) {
168
168
  const key = change.identity.split('.').at(-1);
169
169
  const originalLine = nextLines[change.line];
170
- const indentation = originalLine.match(/^\s*/)![0];
170
+ const indentation = originalLine.match(/^\s*/) [0];
171
171
  // A user comment beside an owned value is still user content. Retain it even though the
172
172
  // owned scalar changes, so reconciliation does not silently erase unrelated explanation.
173
173
  const suffix = commentSuffix(originalLine);
@@ -175,18 +175,18 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
175
175
  }
176
176
  // A line DoFlow placed in the user's own table (marker or dotted form) is deleted outright and its
177
177
  // header kept, so the file returns to its exact bytes. Any other removal leaves a blank line.
178
- const removals = changes.filter((change: any) => change.type === 'remove');
179
- const placedInUserTable = removals.filter((change: any) => placedLine(change.identity, parsed.entries.get(change.identity), parsed.lines));
178
+ const removals = changes.filter((change ) => change.type === 'remove');
179
+ const placedInUserTable = removals.filter((change ) => placedLine(change.identity, parsed.entries.get(change.identity), parsed.lines));
180
180
  for (const change of removals) nextLines[change.line] = placedInUserTable.includes(change) ? null : '';
181
- dropEmptiedTables(nextLines, changes.filter((change: any) => !placedInUserTable.includes(change)), parsed.headers);
181
+ dropEmptiedTables(nextLines, changes.filter((change ) => !placedInUserTable.includes(change)), parsed.headers);
182
182
  // A new key must land INSIDE its own table. Appending at end-of-file only happens to be
183
183
  // correct when that table is the file's last one — otherwise the key silently joins whichever
184
184
  // table trails the file, so `features.hooks` written after an `[mcp_servers.x]` block becomes
185
185
  // `mcp_servers.x.hooks`. placeEntry chooses the spot; genuinely new tables are appended once
186
186
  // each, with all of their keys grouped under a single header.
187
- const insertions: { at: number, line: string }[] = [];
188
- const newTables = new Map<string, string[]>();
189
- for (const change of changes.filter((change: any) => change.type === 'create')) {
187
+ const insertions = [];
188
+ const newTables = new Map ();
189
+ for (const change of changes.filter((change ) => change.type === 'create')) {
190
190
  const placement = placements.get(change.identity);
191
191
  if (placement.at !== undefined) insertions.push({ at: placement.at, line: placement.line });
192
192
  else newTables.set(placement.table, [...(newTables.get(placement.table) || []), placement.line]);
@@ -204,11 +204,11 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
204
204
  }
205
205
  let content = nextLines.join('\n');
206
206
  if (content && !content.endsWith('\n')) content += '\n';
207
- const invalid = newViolation({ file, original, content, identities: changes.map((change: any) => change.identity) });
207
+ const invalid = newViolation({ file, original, content, identities: changes.map((change ) => change.identity) });
208
208
  if (invalid) return { ok: false, status: 'conflict', file, original, changes: [], conflicts: [invalid], managedResources };
209
- const nextManagedResources = managedResources.filter((resource: any) => !isOwnedRecord(resource, scope) || desired.has(resource.identity));
209
+ const nextManagedResources = managedResources.filter((resource ) => !isOwnedRecord(resource, scope) || desired.has(resource.identity));
210
210
  for (const resource of desired.values()) {
211
- const index = nextManagedResources.findIndex((item: any) => isOwnedRecord(item, scope) && item.identity === resource.identity);
211
+ const index = nextManagedResources.findIndex((item ) => isOwnedRecord(item, scope) && item.identity === resource.identity);
212
212
  const updated = { ...resource, target: 'codex', scope, kind: CONFIG_KIND, fingerprint: fingerprint(resource.value) };
213
213
  if (index >= 0) nextManagedResources[index] = updated;
214
214
  else nextManagedResources.push(updated);
@@ -221,17 +221,17 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
221
221
  * it was the file's last table, the blank line the table append puts before a new header goes too.
222
222
  * A table that also receives a created key, or whose header line carries a comment, is kept.
223
223
  * Headers come from the parser's own records, never from re-reading lines. */
224
- function dropEmptiedTables(lines: any[], changes: any[], headerRecords: any[]): void {
225
- const tableOf = (identity: string) => identity.split('.').slice(0, -1).join('.');
226
- const removedTables = new Map<number, string>(changes.filter((change) => change.type === 'remove').map((change): [number, string] => [change.line, tableOf(change.identity)]));
227
- const createdTables = new Set<string>(changes.filter((change) => change.type === 'create').map((change) => tableOf(change.identity)));
228
- const headers: number[] = headerRecords.map((header) => header.line);
224
+ function dropEmptiedTables(lines , changes , headerRecords ) {
225
+ const tableOf = (identity ) => identity.split('.').slice(0, -1).join('.');
226
+ const removedTables = new Map (changes.filter((change) => change.type === 'remove').map((change) => [change.line, tableOf(change.identity)]));
227
+ const createdTables = new Set (changes.filter((change) => change.type === 'create').map((change) => tableOf(change.identity)));
228
+ const headers = headerRecords.map((header) => header.line);
229
229
  headers.forEach((start, position) => {
230
230
  const end = headers[position + 1] ?? lines.length;
231
231
  if (stripComment(lines[start]) !== lines[start]) return;
232
- const body = lines.slice(start + 1, end).map((_: any, offset: number) => start + 1 + offset);
232
+ const body = lines.slice(start + 1, end).map((_ , offset ) => start + 1 + offset);
233
233
  const removedHere = body.filter((index) => removedTables.has(index));
234
- if (!removedHere.length || removedHere.some((index) => createdTables.has(removedTables.get(index) as string))) return;
234
+ if (!removedHere.length || removedHere.some((index) => createdTables.has(removedTables.get(index) ))) return;
235
235
  if (body.some((index) => !removedTables.has(index) && lines[index] !== null && lines[index].trim() !== '')) return;
236
236
  lines[start] = null;
237
237
  for (const index of removedHere) lines[index] = null;
@@ -241,7 +241,7 @@ function dropEmptiedTables(lines: any[], changes: any[], headerRecords: any[]):
241
241
 
242
242
  /** Replace a file through a sibling temporary file, keeping the existing file's permission bits so
243
243
  * a config kept private stays so. A file this creates gets the process's default mode. */
244
- function atomicWrite(file: string, content: string, fsImpl: any = fs): void {
244
+ function atomicWrite(file , content , fsImpl = fs) {
245
245
  fsImpl.mkdirSync(path.dirname(file), { recursive: true });
246
246
  const mode = fsImpl.existsSync(file) ? fsImpl.statSync(file).mode & 0o7777 : null;
247
247
  const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
@@ -254,7 +254,7 @@ function atomicWrite(file: string, content: string, fsImpl: any = fs): void {
254
254
  }
255
255
  }
256
256
 
257
- function applyCodexConfig(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?: boolean, fsImpl?: any } = {}): any {
257
+ function applyCodexConfig(plan , { dryRun = false, fsImpl = fs } = {}) {
258
258
  if (!plan.ok) return { ...plan, applied: false };
259
259
  if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
260
260
  // A removal that leaves nothing behind deletes the file rather than leaving it empty.
@@ -263,7 +263,7 @@ function applyCodexConfig(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?:
263
263
  return { ...plan, applied: true };
264
264
  }
265
265
 
266
- function reconcileCodexConfig(options: any): any {
266
+ function reconcileCodexConfig(options ) {
267
267
  const file = options.file ?? configPath(options);
268
268
  const plan = planCodexConfig({ ...options, file });
269
269
  return applyCodexConfig(plan, options);
@@ -7,19 +7,20 @@
7
7
  // that are not executable. Trust is reported as a prerequisite rather than being bypassed.
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
- const { verifyHookCommands } = require('../hook-commands.ts');
11
- const { mergeHooks } = require('../../helper/settings-merge.ts');
12
- const { atomicWrite } = require('./config.ts');
10
+ const crypto = require('node:crypto');
11
+ const { verifyHookCommands } = require('../hook-commands.js');
12
+ const { mergeHooks } = require('../../helper/settings-merge.js');
13
+ const { atomicWrite } = require('./config.js');
13
14
 
14
15
  const SUPPORTED_EVENTS = new Set([
15
16
  'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact',
16
17
  'UserPromptSubmit', 'SubagentStop', 'Stop', 'SessionStart', 'SubagentStart', 'SessionEnd',
17
18
  ]);
18
19
 
19
- function validationError(errors: string[], message: string): void { errors.push(message); }
20
+ function validationError(errors , message ) { errors.push(message); }
20
21
 
21
- function validateHooksConfig(config: any): { ok: boolean, errors: string[], events: string[] } {
22
- const errors: string[] = [];
22
+ function validateHooksConfig(config ) {
23
+ const errors = [];
23
24
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
24
25
  return { ok: false, errors: ['Hook configuration must be an object'], events: [] };
25
26
  }
@@ -35,7 +36,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
35
36
  validationError(errors, `Hook event '${event}' requires a non-empty array of matcher groups`);
36
37
  continue;
37
38
  }
38
- groups.forEach((group: any, groupIndex: number) => {
39
+ groups.forEach((group , groupIndex ) => {
39
40
  if (!group || typeof group !== 'object' || Array.isArray(group)) {
40
41
  validationError(errors, `Hook event '${event}' group ${groupIndex} must be an object`);
41
42
  return;
@@ -47,7 +48,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
47
48
  validationError(errors, `Hook event '${event}' group ${groupIndex} requires a non-empty hooks array`);
48
49
  return;
49
50
  }
50
- group.hooks.forEach((handler: any, handlerIndex: number) => {
51
+ group.hooks.forEach((handler , handlerIndex ) => {
51
52
  const label = `Hook event '${event}' group ${groupIndex} handler ${handlerIndex}`;
52
53
  if (!handler || typeof handler !== 'object' || Array.isArray(handler)) {
53
54
  validationError(errors, `${label} must be an object`);
@@ -73,9 +74,9 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
73
74
  }
74
75
 
75
76
  /** Maps Claude guardrail intent, never fabricating a Codex equivalent for unavailable events. */
76
- function classifyClaudeGuardrails(claudeHooks: any = {}): { supported: any[], unsupported: any[] } {
77
- const supported: any[] = [];
78
- const unsupported: any[] = [];
77
+ function classifyClaudeGuardrails(claudeHooks = {}) {
78
+ const supported = [];
79
+ const unsupported = [];
79
80
  for (const [event, groups] of Object.entries(claudeHooks || {})) {
80
81
  const item = { sourceEvent: event, targetEvent: event, groups: Array.isArray(groups) ? groups.length : 0 };
81
82
  if (SUPPORTED_EVENTS.has(event)) supported.push(item);
@@ -84,7 +85,7 @@ function classifyClaudeGuardrails(claudeHooks: any = {}): { supported: any[], un
84
85
  return { supported, unsupported };
85
86
  }
86
87
 
87
- function destinationFor(context: any = {}): string {
88
+ function destinationFor(context = {}) {
88
89
  // Preferred: the declared path resolved by the adapter (harnesses.json "paths".hooksFile).
89
90
  if (context.paths?.hooksFile) return context.paths.hooksFile;
90
91
  // Legacy derivation for direct callers passing raw scope inputs; kept byte-compatible with the
@@ -102,20 +103,45 @@ function destinationFor(context: any = {}): string {
102
103
  throw new Error('A hook destination or Codex scope is required');
103
104
  }
104
105
 
105
- function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext = {}, trusted = false, fsImpl = fs }: any = {}): any {
106
+ /** DoFlow's record of a deployed hook script, in the ledger's fingerprint form: sha256 of its bytes. */
107
+ function scriptFingerprint(bytes ) { return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`; }
108
+
109
+ /** The scripts beside the config that differ from the shipped source (or are absent), by name, and
110
+ * those among them a ledger row says were edited since DoFlow wrote them. A script with no row
111
+ * (an install from before scripts were recorded) is DoFlow's to refresh: the config deploy always
112
+ * overwrote it. `rows` are this harness's script rows: { target, fingerprint }. */
113
+ function planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows = [], force = false, fsImpl = fs } ) {
114
+ const sources = new Map (commands.checks.map((check ) => [check.script, check.file] ));
115
+ for (const name of shippedHookScripts(sourceHooksDir, fsImpl)) if (!sources.has(name)) sources.set(name, path.join(sourceHooksDir, name));
116
+ const stale = [];
117
+ const edited = [];
118
+ for (const [name, source] of sources) {
119
+ const target = path.join(destinationHooksDir, name);
120
+ if (fsImpl.existsSync(target)) {
121
+ const current = fsImpl.readFileSync(target);
122
+ if (current.equals(fsImpl.readFileSync(source))) continue;
123
+ const row = rows.find((item ) => path.resolve(item.target) === path.resolve(target));
124
+ if (row && row.fingerprint !== scriptFingerprint(current) && !force) edited.push(name);
125
+ }
126
+ stale.push(name);
127
+ }
128
+ return { names: [...sources.keys()], stale, edited };
129
+ }
130
+
131
+ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext = {}, trusted = false, scriptRows = [], force = false, fsImpl = fs } = {}) {
106
132
  let desired = config;
107
133
  try {
108
134
  if (!desired && sourceFile) desired = JSON.parse(fsImpl.readFileSync(sourceFile, 'utf8'));
109
- } catch (error: any) {
135
+ } catch (error ) {
110
136
  return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${sourceFile}: ${error.message}`] };
111
137
  }
112
138
  const schema = validateHooksConfig(desired);
113
139
  let destination;
114
- try { destination = destinationFor(destinationContext); } catch (error: any) {
140
+ try { destination = destinationFor(destinationContext); } catch (error ) {
115
141
  return { ok: false, status: 'invalid-request', changes: [], errors: [...schema.errors, error.message] };
116
142
  }
117
143
  const commands = verifyHookCommands(desired || {}, { scriptsDir: sourceHooksDir, trusted, fsImpl });
118
- const errors = [...schema.errors, ...commands.checks.filter((check: any) => !check.ok).map((check: any) => `${check.event}: ${check.reason}${check.script ? ` (${check.script})` : ''}`)];
144
+ const errors = [...schema.errors, ...commands.checks.filter((check ) => !check.ok).map((check ) => `${check.event}: ${check.reason}${check.script ? ` (${check.script})` : ''}`)];
119
145
  let mergedDesired = desired;
120
146
  const original = fsImpl.existsSync(destination) ? fsImpl.readFileSync(destination, 'utf8') : '';
121
147
  if (original.trim()) {
@@ -130,25 +156,34 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
130
156
  }
131
157
  const content = schema.ok && mergedDesired ? `${JSON.stringify(mergedDesired, null, 2)}\n` : null;
132
158
  if (errors.length) return { ok: false, status: 'invalid-request', destination, original, changes: [], errors, commands };
133
- const changed = original !== content;
159
+ const configChanged = original !== content;
134
160
  const destinationHooksDir = destinationContext.paths?.hooksDirectory
135
161
  || destinationContext.hooksDir
136
162
  || path.join(path.dirname(destination), 'hooks');
163
+ // The scripts are deployed with the config but are not part of it: a script whose bytes differ
164
+ // from the source is redeployed even when the config is already right.
165
+ const scripts = planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows: scriptRows, force, fsImpl });
166
+ if (scripts.edited.length) {
167
+ return { ok: false, status: 'conflict', destination, original, changes: [], commands,
168
+ errors: scripts.edited.map((name ) => `Codex hook script '${name}' was modified outside DoFlow`) };
169
+ }
170
+ const scriptsOnly = !configChanged && scripts.stale.length > 0;
171
+ const changed = configChanged || scriptsOnly;
137
172
  // The scripts deployCodexHooks copies beside hooks.json, so a backup of this change holds them too.
138
173
  const companionTargets = [...new Set([
139
- ...commands.checks.map((check: any) => path.join(destinationHooksDir, check.script)),
140
- ...shippedHookScripts(sourceHooksDir, fsImpl).map((name: string) => path.join(destinationHooksDir, name)),
174
+ ...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
175
+ ...shippedHookScripts(sourceHooksDir, fsImpl).map((name ) => path.join(destinationHooksDir, name)),
141
176
  ])].sort();
142
- return { ok: true, status: changed ? 'change' : 'unchanged', destination, original, content,
177
+ return { ok: true, status: changed ? 'change' : 'unchanged', destination, original, content, scriptsOnly, scriptNames: scripts.names,
143
178
  changes: changed ? [{ type: fsImpl.existsSync(destination) ? 'update' : 'create', file: destination, companionTargets }] : [],
144
179
  errors: [], commands, trust: commands.trust, scriptsDir: sourceHooksDir, destinationHooksDir };
145
180
  }
146
181
 
147
- function deployCodexHooks(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?: boolean, fsImpl?: any } = {}): any {
182
+ function deployCodexHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
148
183
  if (!plan.ok) return { ...plan, applied: false };
149
184
  if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
150
- atomicWrite(plan.destination, plan.content, fsImpl);
151
- const deployed = new Set<string>();
185
+ if (!plan.scriptsOnly) atomicWrite(plan.destination, plan.content, fsImpl);
186
+ const deployed = new Set ();
152
187
  for (const check of plan.commands.checks) {
153
188
  const target = path.join(plan.destinationHooksDir, check.script);
154
189
  fsImpl.mkdirSync(path.dirname(target), { recursive: true });
@@ -174,16 +209,16 @@ function deployCodexHooks(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?:
174
209
 
175
210
  /** Every file deployCodexHooks copies beside hooks.json: each regular, non-JSON file in the
176
211
  * shipped scripts directory (the command-named scripts are among them). */
177
- function shippedHookScripts(scriptsDir: any, fsImpl: any = fs): string[] {
212
+ function shippedHookScripts(scriptsDir , fsImpl = fs) {
178
213
  if (!scriptsDir || !fsImpl.existsSync(scriptsDir)) return [];
179
214
  return fsImpl.readdirSync(scriptsDir)
180
- .filter((name: string) => !name.endsWith('.json') && fsImpl.statSync(path.join(scriptsDir, name)).isFile());
215
+ .filter((name ) => !name.endsWith('.json') && fsImpl.statSync(path.join(scriptsDir, name)).isFile());
181
216
  }
182
217
 
183
218
  /** Undo deployCodexHooks' script copies. The ledger records only hooks.json, so ownership of a
184
219
  * script is proven by its bytes: one still identical to the shipped source is deleted, one the
185
220
  * user edited or added stays. The directory goes only once it is empty. */
186
- function removeCodexHookScripts({ scriptsDir, destinationHooksDir }: { scriptsDir?: string, destinationHooksDir?: string } = {}, { dryRun = false, fsImpl = fs }: { dryRun?: boolean, fsImpl?: any } = {}): { removed: number } {
221
+ function removeCodexHookScripts({ scriptsDir, destinationHooksDir } = {}, { dryRun = false, fsImpl = fs } = {}) {
187
222
  if (!destinationHooksDir || !fsImpl.existsSync(destinationHooksDir)) return { removed: 0 };
188
223
  let removed = 0;
189
224
  for (const name of shippedHookScripts(scriptsDir, fsImpl)) {
@@ -200,4 +235,4 @@ function removeCodexHookScripts({ scriptsDir, destinationHooksDir }: { scriptsDi
200
235
  return { removed };
201
236
  }
202
237
 
203
- module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planCodexHooks, deployCodexHooks, shippedHookScripts, removeCodexHookScripts };
238
+ module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planCodexHooks, deployCodexHooks, scriptFingerprint, shippedHookScripts, removeCodexHookScripts };