qfai 2.1.1 → 2.2.0

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 (185) hide show
  1. package/README.md +56 -36
  2. package/assets/defaults/agent-routing/qfai-implement.yml +9 -0
  3. package/assets/defaults/agent-routing/qfai-triage.yml +18 -0
  4. package/assets/defaults/workflows/answer-in-session.yml +8 -0
  5. package/assets/defaults/workflows/quarantine-flaky.yml +3 -0
  6. package/assets/defaults/workflows/repair-consistency.yml +1 -0
  7. package/assets/defaults/workflows/run-operation.yml +11 -0
  8. package/assets/defaults/workflows/run-owned-cleanup.yml +12 -0
  9. package/assets/defaults/workflows/write-procedure.yml +18 -0
  10. package/assets/graph/codegraph-native.mjs +174 -0
  11. package/assets/graph/graphify-native.py +72 -0
  12. package/assets/init/.claude/settings.json +9 -9
  13. package/assets/init/.codex/hooks.json +8 -8
  14. package/assets/init/.qfai/assistant/agent/frontend-engineer.md +5 -0
  15. package/assets/init/.qfai/assistant/agent/product-experience-architect.md +4 -0
  16. package/assets/init/.qfai/assistant/agent/product-surface-reviewer.md +7 -0
  17. package/assets/init/.qfai/assistant/agent/requirements-analyst.md +8 -0
  18. package/assets/init/.qfai/assistant/rule/communication.md +18 -8
  19. package/assets/init/.qfai/assistant/rule/constitution.md +31 -15
  20. package/assets/init/.qfai/assistant/rule/drift-protocol.md +119 -2
  21. package/assets/init/.qfai/assistant/rule/references/asset-authoring.md +6 -4
  22. package/assets/init/.qfai/assistant/rule/references/codex-delegation.md +27 -0
  23. package/assets/init/.qfai/assistant/rule/references/griller-recommendations.md +21 -9
  24. package/assets/init/.qfai/assistant/rule/references/worker-edit-boundary.md +48 -0
  25. package/assets/init/.qfai/assistant/rule/review-convergence.md +25 -12
  26. package/assets/init/.qfai/assistant/rule/shared-skill-delegation-baseline.md +16 -25
  27. package/assets/init/.qfai/assistant/rule/shared-skill-operating-baseline.md +16 -11
  28. package/assets/init/.qfai/assistant/rule/ui-definition-protocol.md +6 -1
  29. package/assets/init/.qfai/assistant/rule/workflow.md +2 -1
  30. package/assets/init/.qfai/assistant/skill/qfai-configure/SKILL.md +3 -3
  31. package/assets/init/.qfai/assistant/skill/qfai-discussion/SKILL.md +6 -0
  32. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/discussion-completion-matrix.md +20 -15
  33. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui-bearing-playbook.md +2 -4
  34. package/assets/init/.qfai/assistant/skill/qfai-discussion/references/ui_ux_best_practices.md +1 -6
  35. package/assets/init/.qfai/assistant/skill/qfai-discussion/templates/uiux/00_index.md +0 -16
  36. package/assets/init/.qfai/assistant/skill/qfai-grilling/SKILL.md +30 -20
  37. package/assets/init/.qfai/assistant/skill/qfai-grilling/references/delegated-session.md +4 -3
  38. package/assets/init/.qfai/assistant/skill/qfai-grilling/references/session-record.md +8 -2
  39. package/assets/init/.qfai/assistant/skill/qfai-implement/SKILL.md +16 -3
  40. package/assets/init/.qfai/assistant/skill/qfai-implement/references/parallelization-policy.md +9 -1
  41. package/assets/init/.qfai/assistant/skill/qfai-implement/references/red-admissibility.md +46 -0
  42. package/assets/init/.qfai/assistant/skill/qfai-implement/references/ui-affecting.md +5 -2
  43. package/assets/init/.qfai/assistant/skill/qfai-maintain/SKILL.md +3 -3
  44. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/SKILL.md +12 -6
  45. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/references/migration-guide.md +1 -265
  46. package/assets/init/.qfai/assistant/skill/qfai-migration-v1-to-v2/references/migration-placement.md +273 -0
  47. package/assets/init/.qfai/assistant/skill/qfai-prototyping/SKILL.md +4 -3
  48. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/review-payload-schema.md +2 -3
  49. package/assets/init/.qfai/assistant/skill/qfai-prototyping/references/reviewer-prompt.md +17 -1
  50. package/assets/init/.qfai/assistant/skill/qfai-run/SKILL.md +52 -37
  51. package/assets/init/.qfai/assistant/skill/qfai-run/references/codex-cloud-install.md +80 -0
  52. package/assets/init/.qfai/assistant/skill/qfai-run/references/evidence-delivery.md +322 -0
  53. package/assets/init/.qfai/assistant/skill/qfai-run/references/extraction.md +83 -49
  54. package/assets/init/.qfai/assistant/skill/qfai-run/references/operator-screens.md +226 -11
  55. package/assets/init/.qfai/assistant/skill/qfai-run/references/plan.md +6 -3
  56. package/assets/init/.qfai/assistant/skill/qfai-run/references/reached-step-observations.md +132 -0
  57. package/assets/init/.qfai/assistant/skill/qfai-run/references/stage-points.md +14 -0
  58. package/assets/init/.qfai/assistant/skill/qfai-run/scripts/codex-cloud-install.mjs +117 -0
  59. package/assets/init/.qfai/assistant/skill/qfai-sdd/SKILL.md +4 -3
  60. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/contract-artifact-rules.md +1 -0
  61. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-anti-patterns.md +23 -5
  62. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/design-md-authoring.md +20 -7
  63. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/requirements-decomposition.md +8 -0
  64. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-execution-playbook.md +29 -4
  65. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-phase-checklists.md +1 -1
  66. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-pre-draft-grilling.md +4 -1
  67. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-quality-gate.md +1 -1
  68. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/sdd-triage.md +159 -4
  69. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/spec-traceability-rules.md +14 -2
  70. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-contract-guide.md +146 -0
  71. package/assets/init/.qfai/assistant/skill/qfai-sdd/references/ui-design-contract-normalization.md +4 -29
  72. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/change-request.md +38 -1
  73. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/contracts/ui-contract.sample.yaml +52 -5
  74. package/assets/init/.qfai/assistant/skill/qfai-sdd/templates/spec/decisions.md +12 -0
  75. package/assets/init/.qfai/assistant/skill/qfai-triage/SKILL.md +34 -21
  76. package/assets/init/.qfai/assistant/skill/qfai-verify/SKILL.md +5 -4
  77. package/assets/init/.qfai/assistant/skill/qfai-verify/references/context-load.md +9 -6
  78. package/assets/init/.qfai/assistant/skill/qfai-verify/references/verify-output-contract.md +80 -15
  79. package/assets/init/.qfai/assistant/skill/web-research/SKILL.md +35 -21
  80. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/brave-search/config.toml +2 -4
  81. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/brave-search/mcp-config.json +9 -5
  82. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/firecrawl/.mcp.json +1 -2
  83. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/firecrawl/config.toml +3 -3
  84. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/firecrawl/mcp-config.json +11 -8
  85. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/playwright/config.toml +1 -1
  86. package/assets/init/.qfai/assistant/skill/web-research/mcp-templates/playwright/mcp-config.json +7 -3
  87. package/assets/init/.qfai/assistant/step/common-design-md/STEP.md +33 -20
  88. package/assets/init/.qfai/assistant/step/common-gate-run/STEP.md +124 -8
  89. package/assets/init/.qfai/assistant/step/common-grilling-record/STEP.md +10 -2
  90. package/assets/init/.qfai/assistant/step/common-policy-check/STEP.md +1 -0
  91. package/assets/init/.qfai/assistant/step/common-review-cycle/STEP.md +26 -9
  92. package/assets/init/.qfai/assistant/step/discussion-interview/STEP.md +15 -8
  93. package/assets/init/.qfai/assistant/step/discussion-oq/STEP.md +5 -1
  94. package/assets/init/.qfai/assistant/step/discussion-pack/STEP.md +6 -1
  95. package/assets/init/.qfai/assistant/step/discussion-research/STEP.md +2 -0
  96. package/assets/init/.qfai/assistant/step/discussion-uiux/STEP.md +6 -4
  97. package/assets/init/.qfai/assistant/step/implement-backport/STEP.md +1 -1
  98. package/assets/init/.qfai/assistant/step/implement-benchmark/STEP.md +7 -4
  99. package/assets/init/.qfai/assistant/step/implement-dep-bump/STEP.md +11 -2
  100. package/assets/init/.qfai/assistant/step/implement-diagnose/STEP.md +2 -0
  101. package/assets/init/.qfai/assistant/step/implement-oracle-parity/STEP.md +2 -2
  102. package/assets/init/.qfai/assistant/step/implement-procedure/STEP.md +69 -0
  103. package/assets/init/.qfai/assistant/step/implement-quarantine/STEP.md +24 -7
  104. package/assets/init/.qfai/assistant/step/implement-refactor/STEP.md +2 -2
  105. package/assets/init/.qfai/assistant/step/implement-regression-fix/STEP.md +7 -6
  106. package/assets/init/.qfai/assistant/step/implement-retire/STEP.md +6 -3
  107. package/assets/init/.qfai/assistant/step/implement-revert/STEP.md +4 -3
  108. package/assets/init/.qfai/assistant/step/implement-seam/STEP.md +6 -5
  109. package/assets/init/.qfai/assistant/step/implement-stress-harness/STEP.md +34 -3
  110. package/assets/init/.qfai/assistant/step/implement-sweep/STEP.md +4 -3
  111. package/assets/init/.qfai/assistant/step/implement-tdd/STEP.md +39 -5
  112. package/assets/init/.qfai/assistant/step/implement-tooling/STEP.md +22 -2
  113. package/assets/init/.qfai/assistant/step/maintain-edit/STEP.md +18 -14
  114. package/assets/init/.qfai/assistant/step/prototyping-grill/STEP.md +4 -3
  115. package/assets/init/.qfai/assistant/step/prototyping-loop/STEP.md +4 -0
  116. package/assets/init/.qfai/assistant/step/prototyping-preflight/STEP.md +9 -3
  117. package/assets/init/.qfai/assistant/step/sdd-contract/STEP.md +11 -0
  118. package/assets/init/.qfai/assistant/step/sdd-flow/STEP.md +2 -0
  119. package/assets/init/.qfai/assistant/step/sdd-story/STEP.md +10 -0
  120. package/assets/init/.qfai/assistant/step/sdd-triage/STEP.md +34 -10
  121. package/assets/init/.qfai/assistant/step/triage-answer/STEP.md +7 -6
  122. package/assets/init/.qfai/assistant/step/triage-close/STEP.md +33 -23
  123. package/assets/init/.qfai/assistant/step/triage-cluster/STEP.md +5 -3
  124. package/assets/init/.qfai/assistant/step/triage-decompose/STEP.md +2 -0
  125. package/assets/init/.qfai/assistant/step/triage-dedupe/STEP.md +4 -0
  126. package/assets/init/.qfai/assistant/step/triage-handoff/STEP.md +13 -4
  127. package/assets/init/.qfai/assistant/step/triage-investigate/STEP.md +8 -5
  128. package/assets/init/.qfai/assistant/step/triage-operate/STEP.md +50 -0
  129. package/assets/init/.qfai/assistant/step/triage-owned-cleanup/STEP.md +86 -0
  130. package/assets/init/.qfai/assistant/step/triage-request-info/STEP.md +2 -0
  131. package/assets/init/.qfai/assistant/step/triage-security-intake/STEP.md +6 -3
  132. package/assets/init/.qfai/assistant/step/verify-change-note/STEP.md +1 -0
  133. package/assets/init/.qfai/assistant/step/verify-commit/STEP.md +37 -9
  134. package/assets/init/.qfai/assistant/step/verify-external/STEP.md +5 -2
  135. package/assets/init/.qfai/assistant/step/verify-qfai-gate/STEP.md +14 -16
  136. package/assets/init/.qfai/assistant/step/verify-release-notes/STEP.md +1 -0
  137. package/assets/init/.qfai/assistant/step/verify-repeat-run/STEP.md +46 -4
  138. package/assets/init/.qfai/assistant/step/verify-repo-gate/STEP.md +133 -30
  139. package/assets/init/root/.agents/rules/ai-readable-markdown.md +2 -2
  140. package/assets/init/root/.agents/rules/api-budget.md +187 -8
  141. package/assets/init/root/.agents/rules/document-schema.md +17 -0
  142. package/assets/init/root/.agents/rules/documentation-clarity.md +163 -34
  143. package/assets/init/root/.agents/rules/graph-exploration.md +150 -0
  144. package/assets/init/root/.agents/rules/grilling.md +100 -43
  145. package/assets/init/root/.agents/rules/minimal-implementation.md +3 -3
  146. package/assets/init/root/.agents/rules/reminders.json +27 -18
  147. package/assets/init/root/.agents/rules/session-feedback.md +131 -0
  148. package/assets/init/root/.agents/rules/temporary-files.md +33 -0
  149. package/assets/init/root/.agents/rules/untrusted-content.md +2 -2
  150. package/assets/init/root/.agents/rules/user-questions.md +51 -40
  151. package/assets/init/root/.github/workflows/qfai-docs.yml +3 -3
  152. package/assets/init/root/.github/workflows/qfai-tests.yml +88 -35
  153. package/assets/init/root/.github/workflows/qfai-validate.yml +2 -2
  154. package/assets/init/root/AGENTS.md +24 -22
  155. package/assets/sandbox-templates/default-deny.yaml +9 -3
  156. package/assets/schemas/workflow/extraction.schema.json +89 -5
  157. package/assets/scripts/check-mdschema.mjs +77 -8
  158. package/assets/uix-rev/comparison-review.md +1 -1
  159. package/assets/uix-rev/contracts-review.md +1 -2
  160. package/dist/cli/index.cjs +9444 -10071
  161. package/dist/cli/index.cjs.map +1 -1
  162. package/dist/cli/index.mjs +9421 -10041
  163. package/dist/cli/index.mjs.map +1 -1
  164. package/dist/graph/cli.cjs +1568 -0
  165. package/dist/graph/cli.cjs.map +1 -0
  166. package/dist/graph/cli.d.cts +9 -0
  167. package/dist/graph/cli.d.ts +9 -0
  168. package/dist/graph/cli.mjs +1541 -0
  169. package/dist/graph/cli.mjs.map +1 -0
  170. package/dist/graph/hook.cjs +817 -0
  171. package/dist/graph/hook.cjs.map +1 -0
  172. package/dist/graph/hook.d.cts +9 -0
  173. package/dist/graph/hook.d.ts +9 -0
  174. package/dist/graph/hook.mjs +792 -0
  175. package/dist/graph/hook.mjs.map +1 -0
  176. package/dist/index.cjs +7634 -8517
  177. package/dist/index.cjs.map +1 -1
  178. package/dist/index.d.cts +146 -658
  179. package/dist/index.d.ts +146 -658
  180. package/dist/index.mjs +7462 -8318
  181. package/dist/index.mjs.map +1 -1
  182. package/dist/process-C21nTvkT.d.cts +12 -0
  183. package/dist/process-C21nTvkT.d.ts +12 -0
  184. package/package.json +4 -6
  185. package/assets/init/root/CLAUDE.md +0 -46
@@ -0,0 +1,792 @@
1
+ // src/cli/graphHook.ts
2
+ import { readFileSync } from "fs";
3
+ import path16 from "path";
4
+
5
+ // src/core/init/fsGuards.ts
6
+ import { constants } from "fs";
7
+ import { access, lstat, open, readFile, stat } from "fs/promises";
8
+ import path from "path";
9
+
10
+ // src/core/fs/errno.ts
11
+ function isEnoent(err) {
12
+ return hasErrnoCode(err) && err.code === "ENOENT";
13
+ }
14
+ function hasErrnoCode(err) {
15
+ return err !== null && typeof err === "object" && "code" in err && typeof err.code === "string";
16
+ }
17
+
18
+ // src/core/init/fsGuards.ts
19
+ async function readPinnedRegularFile(filePath, maxBytes) {
20
+ let handle;
21
+ try {
22
+ handle = await open(filePath, OPEN_READ_FLAGS);
23
+ const pinned = await handle.stat();
24
+ if (!pinned.isFile() || pinned.size > maxBytes) {
25
+ return null;
26
+ }
27
+ const buffer = Buffer.alloc(maxBytes + 1);
28
+ let filled = 0;
29
+ while (filled < buffer.length) {
30
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
31
+ if (bytesRead === 0) break;
32
+ filled += bytesRead;
33
+ }
34
+ if (filled > maxBytes) return null;
35
+ return buffer.subarray(0, filled).toString("utf-8");
36
+ } catch (error) {
37
+ const code = error?.code;
38
+ if (code === "ENXIO" || code === "EISDIR") return null;
39
+ throw error;
40
+ } finally {
41
+ await handle?.close();
42
+ }
43
+ }
44
+ async function readPinnedRegularFileBytes(filePath, maxBytes) {
45
+ let handle;
46
+ try {
47
+ handle = await open(filePath, OPEN_READ_FLAGS);
48
+ const pinned = await handle.stat();
49
+ if (!pinned.isFile() || pinned.size > maxBytes) return null;
50
+ const buffer = Buffer.alloc(maxBytes + 1);
51
+ let filled = 0;
52
+ while (filled < buffer.length) {
53
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
54
+ if (bytesRead === 0) break;
55
+ filled += bytesRead;
56
+ }
57
+ if (filled > maxBytes) return null;
58
+ return {
59
+ content: Buffer.from(buffer.subarray(0, filled)),
60
+ mode: pinned.mode & 4095,
61
+ uid: pinned.uid,
62
+ gid: pinned.gid,
63
+ dev: pinned.dev,
64
+ ino: pinned.ino
65
+ };
66
+ } catch (error) {
67
+ const code = error?.code;
68
+ if (code === "ENXIO" || code === "EISDIR") return null;
69
+ throw error;
70
+ } finally {
71
+ await handle?.close();
72
+ }
73
+ }
74
+ var OPEN_READ_FLAGS = typeof constants.O_NONBLOCK === "number" ? constants.O_RDONLY | constants.O_NONBLOCK : constants.O_RDONLY;
75
+ async function safeLstat(target) {
76
+ try {
77
+ return await lstat(target);
78
+ } catch {
79
+ return void 0;
80
+ }
81
+ }
82
+ async function findUnsafeWrapperComponent(destRoot, relativeDir) {
83
+ const segments = relativeDir.split("/");
84
+ for (let depth = 1; depth <= segments.length; depth += 1) {
85
+ const relativePath = segments.slice(0, depth).join("/");
86
+ const stats = await safeLstat(path.join(destRoot, ...segments.slice(0, depth)));
87
+ if (stats === void 0) {
88
+ return void 0;
89
+ }
90
+ if (stats.isSymbolicLink()) {
91
+ return { relativePath, symlink: true };
92
+ }
93
+ if (!stats.isDirectory()) {
94
+ return { relativePath, symlink: false };
95
+ }
96
+ }
97
+ return void 0;
98
+ }
99
+ async function findUnsafeHostFileComponent(destRoot, segments) {
100
+ const parent = segments.slice(0, -1).join("/");
101
+ const unsafeParent = parent === "" ? void 0 : await findUnsafeWrapperComponent(destRoot, parent);
102
+ if (unsafeParent !== void 0) {
103
+ return unsafeParent;
104
+ }
105
+ const leaf = await safeLstat(path.join(destRoot, ...segments));
106
+ return leaf?.isSymbolicLink() === true ? { relativePath: segments.join("/"), symlink: true } : void 0;
107
+ }
108
+
109
+ // src/core/graph/hooks.ts
110
+ function classifyGraphExploration(event, authorize = (decision) => decision) {
111
+ if (event.tool === "write_stdin") return outsideBoundary();
112
+ if (["rg", "grep", "grep_search", "search_files", "file_search"].includes(event.tool.toLowerCase())) {
113
+ return authorize(denyExploration(event.tool, event.path ?? "."));
114
+ }
115
+ const shell = /(?:^|[\\/])(?:pwsh|powershell)(?:\.exe)?$/i.test(event.shell ?? "") || event.tool.toLowerCase() === "powershell" || event.tool === "exec_command" && !event.shell && process.platform === "win32" ? "powershell" : /(?:^|[\\/])cmd(?:\.exe)?$/i.test(event.shell ?? "") ? "cmd" : "posix";
116
+ const commands = shellPipelines(event.command ?? "", shell);
117
+ if (!commands) return failedGraphHook();
118
+ return classifyCommands(commands, authorize) ?? {
119
+ decision: "allow",
120
+ state: "configured",
121
+ message: "No direct exploration was classified."
122
+ };
123
+ }
124
+ function classifyCommands(commands, authorize, depth = 0) {
125
+ const changesDirectory = commands.some(changesGraphDirectory);
126
+ const scopedAuthorization = changesDirectory ? (decision) => graphFallbackUnavailable(
127
+ decision,
128
+ "The command changes its working directory.",
129
+ "Run a single search from this project worktree, then retry the denied action."
130
+ ) : authorize;
131
+ let boundary;
132
+ let allowed;
133
+ for (const words of commands) {
134
+ const decision = classifyCommand(words, scopedAuthorization, depth);
135
+ if (decision?.decision === "deny") return decision;
136
+ if (decision?.state === "outside-boundary") boundary = decision;
137
+ else if (decision) allowed = decision;
138
+ }
139
+ return boundary ?? allowed;
140
+ }
141
+ function changesGraphDirectory(words) {
142
+ let offset = 0;
143
+ while (offset < words.length) {
144
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[offset] ?? "")) {
145
+ offset += 1;
146
+ continue;
147
+ }
148
+ if (!["command", "builtin", "env"].includes(words[offset]?.toLowerCase() ?? "")) break;
149
+ offset += 1;
150
+ if (words[offset] === "-v" || words[offset] === "-V") return false;
151
+ while (words[offset]?.startsWith("-")) offset += 1;
152
+ }
153
+ return ["cd", "chdir", "pushd", "popd", "set-location", "sl"].includes(
154
+ words[offset]?.toLowerCase() ?? ""
155
+ );
156
+ }
157
+ function classifyCommand(words, authorize, depth = 0) {
158
+ if (depth > 16) return failedGraphHook();
159
+ let offset = 0;
160
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[offset] ?? "")) offset += 1;
161
+ if (offset > 0) return classifyCommand(words.slice(offset), authorize, depth + 1);
162
+ const operation = words[0]?.split(/[\\/]/).at(-1)?.replace(/\.exe$/i, "").toLowerCase();
163
+ if (operation === "env" || operation === "command" || operation === "builtin") {
164
+ let offset2 = 1;
165
+ while (words[offset2]?.startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[offset2] ?? "")) {
166
+ offset2 += 1;
167
+ }
168
+ return classifyCommand(words.slice(offset2), authorize, depth + 1);
169
+ }
170
+ if (["bash", "sh", "zsh", "pwsh", "powershell", "cmd"].includes(operation ?? "")) {
171
+ const switchIndex = words.findIndex((word) => /^(?:-[a-z]*c|\/c|-command)$/i.test(word));
172
+ const inner = switchIndex >= 0 ? words[switchIndex + 1] : void 0;
173
+ if (!inner) return outsideBoundary();
174
+ const shell = operation === "pwsh" || operation === "powershell" ? "powershell" : operation === "cmd" ? "cmd" : "posix";
175
+ const commands = shellPipelines(inner, shell);
176
+ return commands ? classifyCommands(commands, authorize, depth + 1) : failedGraphHook();
177
+ }
178
+ if (operation === "rg" || operation === "grep" || operation === "findstr") {
179
+ let offset2 = 1;
180
+ while (/^(?:-[RrinlIHsvwxcFoq]+|--(?:hidden|line-number|ignore-case|fixed-strings|recursive)|\/[SIMOPVNLR]+)$/i.test(
181
+ words[offset2] ?? ""
182
+ ))
183
+ offset2 += 1;
184
+ if (words[offset2] === "--") offset2 += 1;
185
+ const positional = words.slice(offset2);
186
+ const eligible = positional.length > 0 && positional.length <= 2 && !positional.some((word) => word.startsWith("-") || /[$%`<>]/.test(word));
187
+ const target = eligible ? positional[1] ?? "." : JSON.stringify(positional);
188
+ return authorize({ ...denyExploration(operation, target), exceptionEligible: eligible });
189
+ }
190
+ let gitCommand = 1;
191
+ if (operation === "git") {
192
+ while (words[gitCommand]?.startsWith("-")) {
193
+ const option = words[gitCommand];
194
+ gitCommand += ["-C", "-c", "--git-dir", "--work-tree"].includes(option ?? "") ? 2 : 1;
195
+ }
196
+ }
197
+ if (operation === "git" && words[gitCommand]?.toLowerCase() === "grep") {
198
+ const paths = words.indexOf("--");
199
+ const targets = paths >= 0 ? words.slice(paths + 1) : [];
200
+ const eligible = gitCommand === 1 && targets.length <= 1 && (paths >= 0 || words.length === 3) && !targets.some((word) => /[$%`<>]/.test(word));
201
+ return authorize({
202
+ ...denyExploration(
203
+ "git grep",
204
+ eligible ? targets[0] ?? "." : JSON.stringify(words.slice(1))
205
+ ),
206
+ exceptionEligible: eligible
207
+ });
208
+ }
209
+ if (operation === "select-string") {
210
+ const path17 = words.findIndex((word) => /^-(?:literalpath|path)$/i.test(word));
211
+ const targets = [];
212
+ for (let offset2 = path17 + 1; path17 >= 0 && offset2 < words.length; offset2 += 1) {
213
+ const word = words[offset2];
214
+ if (word === void 0 || word.startsWith("-")) break;
215
+ targets.push(word);
216
+ }
217
+ const eligible = targets.length === 1 && !targets.some((word) => /[,$%`<>]/.test(word));
218
+ return authorize({
219
+ ...denyExploration("Select-String", eligible ? targets[0] ?? "." : JSON.stringify(targets)),
220
+ exceptionEligible: eligible
221
+ });
222
+ }
223
+ if (!operation || [
224
+ "echo",
225
+ "printf",
226
+ "write-output",
227
+ "cat",
228
+ "get-content",
229
+ "pnpm",
230
+ "npm",
231
+ "npx",
232
+ "corepack",
233
+ "node",
234
+ "nodejs",
235
+ "python",
236
+ "python3",
237
+ "uv",
238
+ "git",
239
+ "pwd",
240
+ "get-location",
241
+ "ls",
242
+ "dir",
243
+ "sort"
244
+ ].includes(operation)) {
245
+ return void 0;
246
+ }
247
+ return outsideBoundary();
248
+ }
249
+ function outsideBoundary() {
250
+ return {
251
+ decision: "allow",
252
+ state: "outside-boundary",
253
+ message: "Search inside scripts, aliases or continuing input is not verified by this pre-tool boundary."
254
+ };
255
+ }
256
+ function shellPipelines(command, shell) {
257
+ const commands = [];
258
+ let words = [];
259
+ let word = "";
260
+ let quote;
261
+ const flushWord = () => {
262
+ if (word.length) words.push(word);
263
+ word = "";
264
+ };
265
+ for (let index = 0; index < command.length; index += 1) {
266
+ const character = command[index];
267
+ if (character === void 0) continue;
268
+ const next = command[index + 1];
269
+ const escaped = next !== void 0 && (shell === "posix" && character === "\\" && (!quote || quote === '"' && ['"', "\\", "$", "`", "\n"].includes(next)) || shell === "powershell" && character === "`" && quote !== "'" || shell === "cmd" && character === "^" && !quote);
270
+ if (escaped) {
271
+ if (next !== "\n") word += next;
272
+ index += 1;
273
+ continue;
274
+ }
275
+ if (quote) {
276
+ if (character === quote) quote = void 0;
277
+ else word += character;
278
+ } else if (character === '"' || character === "'") {
279
+ quote = character;
280
+ } else if (character === "|" || character === ";" || character === "&" || character === "\n") {
281
+ flushWord();
282
+ if (words.length) commands.push(words);
283
+ words = [];
284
+ } else if (/\s/.test(character)) {
285
+ flushWord();
286
+ } else {
287
+ word += character;
288
+ }
289
+ }
290
+ if (quote) return void 0;
291
+ flushWord();
292
+ if (words.length) commands.push(words);
293
+ return commands;
294
+ }
295
+ function denyExploration(operation, target) {
296
+ const domain = /(?:^|[\\/])(?:docs?|spec|adr|requirements)(?:[\\/]|$)|\.md$/i.test(target) ? "documents" : "code";
297
+ return {
298
+ decision: "deny",
299
+ state: "configured",
300
+ operation,
301
+ target,
302
+ domain,
303
+ message: `Exploration redirected: ${operation} over ${JSON.stringify(target)} was denied. Use ${domain === "code" ? 'CodeGraph for symbols, callers and change impact: node .qfai/tools/graph-cli.mjs code "<question>"' : 'Graphify for document concepts and rationale: node .qfai/tools/graph-cli.mjs documents "<question>"'}. Prepare the required analysis through the CLI. For AI-decided scoped search, run node .qfai/tools/graph-cli.mjs fallback --attempt-id <observed-UUID> --question "<unresolved question>" --reason "<graph limitation>". The fallback command attempts the relevant graph before granting a matching search. Human approval is not required for this search.`
304
+ };
305
+ }
306
+ function graphFallbackUnavailable(decision, cause, recovery) {
307
+ return {
308
+ ...decision,
309
+ exceptionEligible: false,
310
+ fallbackUnavailable: true,
311
+ message: `Exploration redirected: ${decision.operation ?? "search"} was denied. Search fallback unavailable: ${cause} ${recovery} Use node .qfai/tools/graph-cli.mjs ${decision.domain === "documents" ? "documents" : "code"} "<question>" for graph exploration.`
312
+ };
313
+ }
314
+ function decodeGraphHookEvent(input) {
315
+ if (!object(input)) return void 0;
316
+ const snake = ["tool_name", "tool_input", "session_id"].some((key) => key in input);
317
+ const camel = ["toolName", "toolArgs", "sessionId"].some((key) => key in input);
318
+ if (snake && camel) return void 0;
319
+ const tool = input.tool_name ?? input.toolName;
320
+ let payload = input.tool_input ?? input.toolArgs;
321
+ if (typeof payload === "string") {
322
+ try {
323
+ payload = JSON.parse(payload);
324
+ } catch {
325
+ return void 0;
326
+ }
327
+ }
328
+ if (typeof tool !== "string" || !tool.trim() || !object(payload)) return void 0;
329
+ const session = input.session_id ?? input.sessionId;
330
+ if (session !== void 0 && (typeof session !== "string" || !session.trim()) || input.session_id !== void 0 && input.sessionId !== void 0 && input.session_id !== input.sessionId)
331
+ return void 0;
332
+ const command = payload.command ?? payload.cmd;
333
+ const target = payload.path ?? payload.includePattern;
334
+ const workdir = payload.workdir;
335
+ const shell = payload.shell;
336
+ if (target !== void 0 && (typeof target !== "string" || !target.trim())) return void 0;
337
+ if (workdir !== void 0 && (typeof workdir !== "string" || !workdir.trim())) return void 0;
338
+ if (shell !== void 0 && (typeof shell !== "string" || !shell.trim())) return void 0;
339
+ if (["bash", "powershell", "exec_command"].includes(tool.toLowerCase()) && typeof command !== "string")
340
+ return void 0;
341
+ return {
342
+ tool,
343
+ ...typeof command === "string" ? { command } : {},
344
+ ...typeof target === "string" ? { path: target } : {},
345
+ ...typeof workdir === "string" ? { workdir } : {},
346
+ ...typeof shell === "string" ? { shell } : {},
347
+ ...typeof input.cwd === "string" ? { cwd: input.cwd } : {},
348
+ ...typeof session === "string" ? { session } : {}
349
+ };
350
+ }
351
+ function failedGraphHook() {
352
+ return {
353
+ decision: "deny",
354
+ state: "failed",
355
+ message: "Graph protection failed: invalid pre-tool input. Repair hook configuration before direct exploration."
356
+ };
357
+ }
358
+ function enforceGraphExploration(input, configuration) {
359
+ if (!object(configuration) || Object.keys(configuration).length !== 2 || configuration.kind !== "graph-enforcement" || configuration.denyExploration !== true)
360
+ return failedGraphHook();
361
+ const event = decodeGraphHookEvent(input);
362
+ return event ? classifyGraphExploration(event) : failedGraphHook();
363
+ }
364
+ function graphHookOutput(host, decision) {
365
+ if (decision.decision === "allow") return {};
366
+ return host === "copilot" ? { permissionDecision: "deny", permissionDecisionReason: decision.message } : {
367
+ hookSpecificOutput: {
368
+ hookEventName: "PreToolUse",
369
+ permissionDecision: "deny",
370
+ permissionDecisionReason: decision.message
371
+ }
372
+ };
373
+ }
374
+ function object(value) {
375
+ return typeof value === "object" && value !== null && !Array.isArray(value);
376
+ }
377
+
378
+ // src/core/graph/fallbackState.ts
379
+ import { randomUUID as randomUUID7 } from "crypto";
380
+ import { rename as rename4, unlink as unlink3 } from "fs/promises";
381
+ import path15 from "path";
382
+
383
+ // src/core/graph/locks.ts
384
+ import { randomUUID as randomUUID2 } from "crypto";
385
+ import { unlink } from "fs/promises";
386
+ import path5 from "path";
387
+
388
+ // src/core/graph/layout.ts
389
+ import path2 from "path";
390
+ function graphExecutionEnvironment() {
391
+ return { platform: process.platform, architecture: process.arch };
392
+ }
393
+ function graphEnvironmentKey(environment = graphExecutionEnvironment()) {
394
+ if (!/^[a-z0-9]+$/i.test(environment.platform) || !/^[a-z0-9]+$/i.test(environment.architecture))
395
+ throw new Error("Unsupported graph execution environment.");
396
+ return `${environment.platform}-${environment.architecture}`;
397
+ }
398
+
399
+ // src/core/graph/localFiles.ts
400
+ import { lstat as lstat3, mkdir as mkdir2, open as open2 } from "fs/promises";
401
+ import path4 from "path";
402
+
403
+ // src/core/graph/storage.ts
404
+ import { randomUUID } from "crypto";
405
+ import { lstat as lstat2, mkdir, readdir, rename, rm } from "fs/promises";
406
+ import path3 from "path";
407
+ async function assertGraphStoragePath(root, directory) {
408
+ const relative = path3.relative(root, directory);
409
+ const parts = relative.split(path3.sep);
410
+ if (path3.isAbsolute(relative) || parts[0] !== ".qfai" || parts[1] !== "tools" || parts.includes("..")) {
411
+ throw new Error("Graph storage must stay inside project-local .qfai/tools.");
412
+ }
413
+ for (let depth = 1; depth <= parts.length; depth += 1) {
414
+ try {
415
+ const stats = await lstat2(path3.join(root, ...parts.slice(0, depth)));
416
+ if (stats.isSymbolicLink() || !stats.isDirectory())
417
+ throw new Error(
418
+ "Graph storage has an unsafe path component; retain it and repair the project configuration."
419
+ );
420
+ } catch (cause) {
421
+ if (isEnoent(cause)) return;
422
+ throw cause;
423
+ }
424
+ }
425
+ }
426
+
427
+ // src/core/graph/localFiles.ts
428
+ var GRAPH_STATE_BYTES = 32768;
429
+ var GRAPH_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
430
+ var GraphLocalStateError = class extends Error {
431
+ constructor(message, recovery, options) {
432
+ super(message, options);
433
+ this.recovery = recovery;
434
+ }
435
+ };
436
+ async function graphStateDirectory(root) {
437
+ const directory = path4.join(root, ".qfai", "tools", "state", graphEnvironmentKey());
438
+ await assertGraphStoragePath(root, directory);
439
+ await mkdir2(directory, { recursive: true });
440
+ await assertGraphStoragePath(root, directory);
441
+ return directory;
442
+ }
443
+ function invalidGraphState() {
444
+ return new GraphLocalStateError(
445
+ "Project graph permission state is invalid.",
446
+ "Retain the local state and repair .qfai/tools/state before retrying the graph CLI or hook."
447
+ );
448
+ }
449
+ function graphStateText(value, maximum) {
450
+ return typeof value === "string" && value.trim().length > 0 && value.length <= maximum && !value.includes("\0");
451
+ }
452
+ async function readGraphLocalFile(file) {
453
+ try {
454
+ const before = await lstat3(file);
455
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
456
+ throw invalidGraphState();
457
+ const pinned = await readPinnedRegularFileBytes(file, GRAPH_STATE_BYTES);
458
+ const after = await lstat3(file);
459
+ if (!pinned || before.dev !== pinned.dev || before.ino !== pinned.ino || after.dev !== pinned.dev || after.ino !== pinned.ino || !after.isFile() || after.isSymbolicLink() || after.nlink !== 1)
460
+ throw invalidGraphState();
461
+ return pinned.content.toString("utf8");
462
+ } catch (cause) {
463
+ if (isEnoent(cause)) return void 0;
464
+ throw cause;
465
+ }
466
+ }
467
+ async function writeGraphLocalFile(file, value) {
468
+ const text = JSON.stringify(value);
469
+ if (Buffer.byteLength(text) > GRAPH_STATE_BYTES) throw invalidGraphState();
470
+ const handle = await open2(file, "wx", 384);
471
+ try {
472
+ await handle.writeFile(text, "utf8");
473
+ } finally {
474
+ await handle.close();
475
+ }
476
+ }
477
+
478
+ // src/core/graph/locks.ts
479
+ async function withGraphLock(root, kind, action) {
480
+ const directory = await graphStateDirectory(root);
481
+ const file = path5.join(directory, `${kind}.lock`);
482
+ const owner = {
483
+ kind: "graph-lock",
484
+ pid: process.pid,
485
+ createdAt: Date.now(),
486
+ token: randomUUID2()
487
+ };
488
+ try {
489
+ await writeGraphLocalFile(file, owner);
490
+ } catch (cause) {
491
+ if (!hasErrnoCode(cause) || cause.code !== "EEXIST") throw cause;
492
+ const text = await readGraphLocalFile(file);
493
+ let active = false;
494
+ try {
495
+ const existing = text === void 0 ? void 0 : JSON.parse(text);
496
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing))
497
+ throw new Error("Invalid graph lock metadata.", { cause });
498
+ const value = existing;
499
+ if (Object.keys(value).length !== 4 || value.kind !== "graph-lock" || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt < 0 || typeof value.token !== "string" || !GRAPH_UUID.test(value.token))
500
+ throw new Error("Invalid graph lock metadata.", { cause });
501
+ try {
502
+ process.kill(value.pid, 0);
503
+ active = true;
504
+ } catch (liveness) {
505
+ if (!hasErrnoCode(liveness) || liveness.code !== "ESRCH") active = true;
506
+ }
507
+ } catch {
508
+ }
509
+ throw new GraphLocalStateError(
510
+ `Project graph ${kind} lock ${active ? "is contended" : "requires manual recovery"}.`,
511
+ active ? `Retry after the active worktree ${kind} operation finishes; retain its lock.` : `Retain and inspect .qfai/tools/state/${graphEnvironmentKey()}/${kind}.lock; remove only a confirmed abandoned worktree lock before retrying.`,
512
+ { cause }
513
+ );
514
+ }
515
+ try {
516
+ return await action();
517
+ } finally {
518
+ await releaseGraphLock(file, JSON.stringify(owner));
519
+ }
520
+ }
521
+ async function releaseGraphLock(file, owner) {
522
+ const text = await readGraphLocalFile(file);
523
+ if (text !== owner)
524
+ throw new GraphLocalStateError(
525
+ "Project graph lock ownership changed.",
526
+ "Retain the lock and repair the worktree graph state before retrying."
527
+ );
528
+ await unlink(file);
529
+ }
530
+
531
+ // src/core/graph/permissionRecords.ts
532
+ import { opendir, unlink as unlink2 } from "fs/promises";
533
+ import path6 from "path";
534
+ async function readGraphSearchRecords(root, directory) {
535
+ const records = [];
536
+ const entries = await opendir(directory);
537
+ for await (const entry of entries) {
538
+ if (entry.name === "state.lock" || entry.name === "analysis.lock") continue;
539
+ if (records.length >= 128) throw fullGraphState();
540
+ const match = /^(.+)\.(pending|grant|draft)\.json$/.exec(entry.name);
541
+ if (!match || !GRAPH_UUID.test(match[1] ?? "")) throw invalidGraphState();
542
+ const file = path6.join(directory, entry.name);
543
+ const text = await readGraphLocalFile(file);
544
+ if (text === void 0) throw invalidGraphState();
545
+ let input;
546
+ try {
547
+ input = JSON.parse(text);
548
+ } catch {
549
+ throw invalidGraphState();
550
+ }
551
+ const record = parseGraphSearchRecord(input, root);
552
+ if (!record || record.id !== match[1] || (match[2] === "pending" ? record.kind !== "pending" : record.kind !== "grant"))
553
+ throw invalidGraphState();
554
+ records.push({ file, record, draft: match[2] === "draft" });
555
+ }
556
+ const current = [];
557
+ for (const item of records) {
558
+ if (Date.now() >= item.record.expiresAt) await unlink2(item.file);
559
+ else current.push(item);
560
+ }
561
+ return current;
562
+ }
563
+ function fullGraphState() {
564
+ return new GraphLocalStateError(
565
+ "Project graph permission state is full.",
566
+ "Retry after pending attempts or search grants expire; retain valid worktree state."
567
+ );
568
+ }
569
+ function parseGraphSearchRecord(input, root) {
570
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
571
+ const value = input;
572
+ const fields = [
573
+ "id",
574
+ "kind",
575
+ "domain",
576
+ "target",
577
+ "operation",
578
+ "worktree",
579
+ "session",
580
+ "createdAt",
581
+ "expiresAt"
582
+ ];
583
+ if (value.kind === "grant") fields.push("reason");
584
+ if (Object.keys(value).length !== fields.length || fields.some((field) => !Object.hasOwn(value, field)) || typeof value.id !== "string" || !GRAPH_UUID.test(value.id) || value.kind !== "pending" && value.kind !== "grant" || value.domain !== "code" && value.domain !== "documents" || !graphStateText(value.target, 4096) || !graphStateText(value.operation, 128) || !graphStateText(value.worktree, 4096) || value.worktree !== path6.resolve(root) || !graphStateText(value.session, 4096) || typeof value.createdAt !== "number" || typeof value.expiresAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt < 0 || value.createdAt > Date.now() || !Number.isSafeInteger(value.expiresAt) || value.expiresAt !== value.createdAt + (value.kind === "pending" ? 6e5 : 6e4))
585
+ return void 0;
586
+ const scope = {
587
+ id: value.id,
588
+ domain: value.domain,
589
+ target: value.target,
590
+ operation: value.operation,
591
+ worktree: value.worktree,
592
+ session: value.session,
593
+ createdAt: value.createdAt,
594
+ expiresAt: value.expiresAt
595
+ };
596
+ if (value.kind === "pending") return { ...scope, kind: "pending" };
597
+ return graphStateText(value.reason, 2048) ? { ...scope, kind: "grant", reason: value.reason } : void 0;
598
+ }
599
+
600
+ // src/core/graph/codePreparation.ts
601
+ import { randomUUID as randomUUID4 } from "crypto";
602
+ import { mkdir as mkdir5, rm as rm4 } from "fs/promises";
603
+ import path11 from "path";
604
+
605
+ // src/shared/assets.ts
606
+ import { existsSync } from "fs";
607
+ import path7 from "path";
608
+ import { fileURLToPath } from "url";
609
+ var INIT_ASSETS_SENTINELS = [
610
+ path7.join(".qfai", "assistant"),
611
+ path7.join("root", "qfai.config.yaml")
612
+ ];
613
+
614
+ // src/core/graph/process.ts
615
+ import { execFile } from "child_process";
616
+ import { access as access2, lstat as lstat4, realpath } from "fs/promises";
617
+ import path8 from "path";
618
+ import { promisify } from "util";
619
+ var execute = promisify(execFile);
620
+
621
+ // src/core/graph/reuse.ts
622
+ import os from "os";
623
+ import path10 from "path";
624
+ import { randomUUID as randomUUID3 } from "crypto";
625
+ import {
626
+ cp,
627
+ lstat as lstat6,
628
+ mkdir as mkdir4,
629
+ readFile as readFile2,
630
+ readdir as readdir3,
631
+ realpath as realpath2,
632
+ rename as rename2,
633
+ rm as rm3,
634
+ writeFile
635
+ } from "fs/promises";
636
+
637
+ // src/core/graph/analysis.ts
638
+ import path9 from "path";
639
+ import { copyFile, lstat as lstat5, mkdir as mkdir3, readdir as readdir2, rm as rm2 } from "fs/promises";
640
+
641
+ // src/core/graph/documents.ts
642
+ import { mkdir as mkdir7, readFile as readFile3, rm as rm6, writeFile as writeFile3 } from "fs/promises";
643
+ import { randomUUID as randomUUID6 } from "crypto";
644
+
645
+ // src/core/graph/documentReuse.ts
646
+ import { randomUUID as randomUUID5 } from "crypto";
647
+ import { cp as cp2, lstat as lstat7, mkdir as mkdir6, readdir as readdir4, rename as rename3, rm as rm5, writeFile as writeFile2 } from "fs/promises";
648
+ import path12 from "path";
649
+
650
+ // src/core/graph/documents.ts
651
+ import path13 from "path";
652
+
653
+ // src/core/graph/query.ts
654
+ import { readFile as readFile4 } from "fs/promises";
655
+ import path14 from "path";
656
+
657
+ // src/core/graph/fallbackState.ts
658
+ async function classifyStoredGraphExploration(root, event) {
659
+ const decision = classifyGraphExploration(event);
660
+ if (decision.decision !== "deny" || decision.state !== "configured") return decision;
661
+ if (decision.exceptionEligible === false)
662
+ return decision.fallbackUnavailable ? decision : graphFallbackUnavailable(
663
+ decision,
664
+ "The search target or operation cannot be scoped.",
665
+ "Run a single search with one explicit project-relative target, then retry the denied action."
666
+ );
667
+ if (!event.cwd || !graphStateText(event.session, 4096))
668
+ return graphFallbackUnavailable(
669
+ decision,
670
+ "Observed session and worktree are required.",
671
+ "Enable session and cwd in the host event, run qfai init, then retry the denied action from this project worktree."
672
+ );
673
+ if (path15.resolve(event.cwd) !== path15.resolve(root))
674
+ return graphFallbackUnavailable(
675
+ decision,
676
+ "The observed worktree differs from this project worktree.",
677
+ "Run the denied action from this project worktree, or run qfai init in the intended worktree before retrying."
678
+ );
679
+ if (event.workdir && path15.resolve(event.cwd, event.workdir) !== path15.resolve(root))
680
+ return graphFallbackUnavailable(
681
+ decision,
682
+ "The requested workdir differs from this project worktree.",
683
+ "Set workdir to this project worktree and retry the denied action."
684
+ );
685
+ if (!decision.target || !decision.operation || !decision.domain) return decision;
686
+ const worktree = path15.resolve(root);
687
+ const session = event.session;
688
+ return withGraphLock(root, "state", async () => {
689
+ const directory = await graphStateDirectory(root);
690
+ const records = await readGraphSearchRecords(root, directory);
691
+ const classified = classifyGraphExploration(event, (candidate) => {
692
+ if (candidate.decision !== "deny" || candidate.exceptionEligible === false) return candidate;
693
+ const grant = records.find(
694
+ (item) => !item.draft && item.record.kind === "grant" && item.record.target === candidate.target && item.record.operation === candidate.operation && item.record.domain === candidate.domain && item.record.worktree === worktree && item.record.session === session && Date.now() < item.record.expiresAt
695
+ );
696
+ return grant?.record.kind === "grant" ? {
697
+ ...candidate,
698
+ decision: "allow",
699
+ message: `AI-decided search fallback: ${grant.record.reason}`
700
+ } : candidate;
701
+ });
702
+ if (classified.decision !== "deny" || classified.state !== "configured" || classified.exceptionEligible === false || !classified.target || !classified.operation || !classified.domain)
703
+ return classified;
704
+ const observed = {
705
+ target: classified.target,
706
+ operation: classified.operation,
707
+ domain: classified.domain,
708
+ worktree,
709
+ session
710
+ };
711
+ if (records.length >= 128) throw fullGraphState();
712
+ const createdAt = Date.now();
713
+ const pending = {
714
+ ...observed,
715
+ id: randomUUID7(),
716
+ kind: "pending",
717
+ createdAt,
718
+ expiresAt: createdAt + 6e5
719
+ };
720
+ await writeGraphLocalFile(path15.join(directory, `${pending.id}.pending.json`), pending);
721
+ return { ...classified, message: `${classified.message} Observed attempt: ${pending.id}.` };
722
+ });
723
+ }
724
+
725
+ // src/cli/graphHook.ts
726
+ function hostName(value) {
727
+ return value === "claude" || value === "codex" || value === "copilot" || value === "vscode";
728
+ }
729
+ async function runInstalledGraphHook(root, args, io = {
730
+ read: () => JSON.parse(readFileSync(0, "utf8")),
731
+ output: (text) => {
732
+ process.stdout.write(text);
733
+ },
734
+ error: (text) => {
735
+ process.stderr.write(text);
736
+ },
737
+ fail: () => {
738
+ process.exitCode = 2;
739
+ }
740
+ }) {
741
+ let input;
742
+ try {
743
+ input = io.read();
744
+ } catch {
745
+ input = void 0;
746
+ }
747
+ const host = args[0] === "shared" ? wireHost(input) : args[0];
748
+ if (!hostName(host)) {
749
+ io.error("Graph protection unsupported: select a supported host adapter.\n");
750
+ io.fail();
751
+ return;
752
+ }
753
+ let decision = failedGraphHook();
754
+ try {
755
+ const components = [".qfai", "tools", "graph-enforcement.json"];
756
+ if (!await findUnsafeHostFileComponent(root, components)) {
757
+ const text = await readPinnedRegularFile(path16.join(root, ...components), 4096);
758
+ if (text !== null) decision = enforceGraphExploration(input, JSON.parse(text));
759
+ }
760
+ } catch {
761
+ }
762
+ if (decision.decision === "deny" && decision.state === "configured") {
763
+ const event = decodeGraphHookEvent(input);
764
+ if (event) {
765
+ try {
766
+ decision = await classifyStoredGraphExploration(root, event);
767
+ } catch (cause) {
768
+ decision = failedGraphHook();
769
+ if (cause instanceof GraphLocalStateError)
770
+ decision.message = `${cause.message} ${cause.recovery}`;
771
+ }
772
+ }
773
+ }
774
+ if (decision.decision === "deny")
775
+ io.output(JSON.stringify(graphHookOutput(host, decision)) + "\n");
776
+ if (decision.state === "failed") {
777
+ io.error("Graph protection failed. Repair the project hook and enforcement configuration.\n");
778
+ io.fail();
779
+ }
780
+ }
781
+ function wireHost(input) {
782
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
783
+ const value = input;
784
+ const snake = ["tool_name", "tool_input", "session_id"].some((key) => key in value);
785
+ const camel = ["toolName", "toolArgs", "sessionId"].some((key) => key in value);
786
+ if (snake === camel) return void 0;
787
+ return camel ? "copilot" : "vscode";
788
+ }
789
+ export {
790
+ runInstalledGraphHook
791
+ };
792
+ //# sourceMappingURL=hook.mjs.map