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,817 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/cli/graphHook.ts
31
+ var graphHook_exports = {};
32
+ __export(graphHook_exports, {
33
+ runInstalledGraphHook: () => runInstalledGraphHook
34
+ });
35
+ module.exports = __toCommonJS(graphHook_exports);
36
+ var import_node_fs3 = require("fs");
37
+ var import_node_path16 = __toESM(require("path"), 1);
38
+
39
+ // src/core/init/fsGuards.ts
40
+ var import_node_fs = require("fs");
41
+ var import_promises = require("fs/promises");
42
+ var import_node_path = __toESM(require("path"), 1);
43
+
44
+ // src/core/fs/errno.ts
45
+ function isEnoent(err) {
46
+ return hasErrnoCode(err) && err.code === "ENOENT";
47
+ }
48
+ function hasErrnoCode(err) {
49
+ return err !== null && typeof err === "object" && "code" in err && typeof err.code === "string";
50
+ }
51
+
52
+ // src/core/init/fsGuards.ts
53
+ async function readPinnedRegularFile(filePath, maxBytes) {
54
+ let handle;
55
+ try {
56
+ handle = await (0, import_promises.open)(filePath, OPEN_READ_FLAGS);
57
+ const pinned = await handle.stat();
58
+ if (!pinned.isFile() || pinned.size > maxBytes) {
59
+ return null;
60
+ }
61
+ const buffer = Buffer.alloc(maxBytes + 1);
62
+ let filled = 0;
63
+ while (filled < buffer.length) {
64
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
65
+ if (bytesRead === 0) break;
66
+ filled += bytesRead;
67
+ }
68
+ if (filled > maxBytes) return null;
69
+ return buffer.subarray(0, filled).toString("utf-8");
70
+ } catch (error) {
71
+ const code = error?.code;
72
+ if (code === "ENXIO" || code === "EISDIR") return null;
73
+ throw error;
74
+ } finally {
75
+ await handle?.close();
76
+ }
77
+ }
78
+ async function readPinnedRegularFileBytes(filePath, maxBytes) {
79
+ let handle;
80
+ try {
81
+ handle = await (0, import_promises.open)(filePath, OPEN_READ_FLAGS);
82
+ const pinned = await handle.stat();
83
+ if (!pinned.isFile() || pinned.size > maxBytes) return null;
84
+ const buffer = Buffer.alloc(maxBytes + 1);
85
+ let filled = 0;
86
+ while (filled < buffer.length) {
87
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
88
+ if (bytesRead === 0) break;
89
+ filled += bytesRead;
90
+ }
91
+ if (filled > maxBytes) return null;
92
+ return {
93
+ content: Buffer.from(buffer.subarray(0, filled)),
94
+ mode: pinned.mode & 4095,
95
+ uid: pinned.uid,
96
+ gid: pinned.gid,
97
+ dev: pinned.dev,
98
+ ino: pinned.ino
99
+ };
100
+ } catch (error) {
101
+ const code = error?.code;
102
+ if (code === "ENXIO" || code === "EISDIR") return null;
103
+ throw error;
104
+ } finally {
105
+ await handle?.close();
106
+ }
107
+ }
108
+ var OPEN_READ_FLAGS = typeof import_node_fs.constants.O_NONBLOCK === "number" ? import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_NONBLOCK : import_node_fs.constants.O_RDONLY;
109
+ async function safeLstat(target) {
110
+ try {
111
+ return await (0, import_promises.lstat)(target);
112
+ } catch {
113
+ return void 0;
114
+ }
115
+ }
116
+ async function findUnsafeWrapperComponent(destRoot, relativeDir) {
117
+ const segments = relativeDir.split("/");
118
+ for (let depth = 1; depth <= segments.length; depth += 1) {
119
+ const relativePath = segments.slice(0, depth).join("/");
120
+ const stats = await safeLstat(import_node_path.default.join(destRoot, ...segments.slice(0, depth)));
121
+ if (stats === void 0) {
122
+ return void 0;
123
+ }
124
+ if (stats.isSymbolicLink()) {
125
+ return { relativePath, symlink: true };
126
+ }
127
+ if (!stats.isDirectory()) {
128
+ return { relativePath, symlink: false };
129
+ }
130
+ }
131
+ return void 0;
132
+ }
133
+ async function findUnsafeHostFileComponent(destRoot, segments) {
134
+ const parent = segments.slice(0, -1).join("/");
135
+ const unsafeParent = parent === "" ? void 0 : await findUnsafeWrapperComponent(destRoot, parent);
136
+ if (unsafeParent !== void 0) {
137
+ return unsafeParent;
138
+ }
139
+ const leaf = await safeLstat(import_node_path.default.join(destRoot, ...segments));
140
+ return leaf?.isSymbolicLink() === true ? { relativePath: segments.join("/"), symlink: true } : void 0;
141
+ }
142
+
143
+ // src/core/graph/hooks.ts
144
+ function classifyGraphExploration(event, authorize = (decision) => decision) {
145
+ if (event.tool === "write_stdin") return outsideBoundary();
146
+ if (["rg", "grep", "grep_search", "search_files", "file_search"].includes(event.tool.toLowerCase())) {
147
+ return authorize(denyExploration(event.tool, event.path ?? "."));
148
+ }
149
+ 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";
150
+ const commands = shellPipelines(event.command ?? "", shell);
151
+ if (!commands) return failedGraphHook();
152
+ return classifyCommands(commands, authorize) ?? {
153
+ decision: "allow",
154
+ state: "configured",
155
+ message: "No direct exploration was classified."
156
+ };
157
+ }
158
+ function classifyCommands(commands, authorize, depth = 0) {
159
+ const changesDirectory = commands.some(changesGraphDirectory);
160
+ const scopedAuthorization = changesDirectory ? (decision) => graphFallbackUnavailable(
161
+ decision,
162
+ "The command changes its working directory.",
163
+ "Run a single search from this project worktree, then retry the denied action."
164
+ ) : authorize;
165
+ let boundary;
166
+ let allowed;
167
+ for (const words of commands) {
168
+ const decision = classifyCommand(words, scopedAuthorization, depth);
169
+ if (decision?.decision === "deny") return decision;
170
+ if (decision?.state === "outside-boundary") boundary = decision;
171
+ else if (decision) allowed = decision;
172
+ }
173
+ return boundary ?? allowed;
174
+ }
175
+ function changesGraphDirectory(words) {
176
+ let offset = 0;
177
+ while (offset < words.length) {
178
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[offset] ?? "")) {
179
+ offset += 1;
180
+ continue;
181
+ }
182
+ if (!["command", "builtin", "env"].includes(words[offset]?.toLowerCase() ?? "")) break;
183
+ offset += 1;
184
+ if (words[offset] === "-v" || words[offset] === "-V") return false;
185
+ while (words[offset]?.startsWith("-")) offset += 1;
186
+ }
187
+ return ["cd", "chdir", "pushd", "popd", "set-location", "sl"].includes(
188
+ words[offset]?.toLowerCase() ?? ""
189
+ );
190
+ }
191
+ function classifyCommand(words, authorize, depth = 0) {
192
+ if (depth > 16) return failedGraphHook();
193
+ let offset = 0;
194
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[offset] ?? "")) offset += 1;
195
+ if (offset > 0) return classifyCommand(words.slice(offset), authorize, depth + 1);
196
+ const operation = words[0]?.split(/[\\/]/).at(-1)?.replace(/\.exe$/i, "").toLowerCase();
197
+ if (operation === "env" || operation === "command" || operation === "builtin") {
198
+ let offset2 = 1;
199
+ while (words[offset2]?.startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[offset2] ?? "")) {
200
+ offset2 += 1;
201
+ }
202
+ return classifyCommand(words.slice(offset2), authorize, depth + 1);
203
+ }
204
+ if (["bash", "sh", "zsh", "pwsh", "powershell", "cmd"].includes(operation ?? "")) {
205
+ const switchIndex = words.findIndex((word) => /^(?:-[a-z]*c|\/c|-command)$/i.test(word));
206
+ const inner = switchIndex >= 0 ? words[switchIndex + 1] : void 0;
207
+ if (!inner) return outsideBoundary();
208
+ const shell = operation === "pwsh" || operation === "powershell" ? "powershell" : operation === "cmd" ? "cmd" : "posix";
209
+ const commands = shellPipelines(inner, shell);
210
+ return commands ? classifyCommands(commands, authorize, depth + 1) : failedGraphHook();
211
+ }
212
+ if (operation === "rg" || operation === "grep" || operation === "findstr") {
213
+ let offset2 = 1;
214
+ while (/^(?:-[RrinlIHsvwxcFoq]+|--(?:hidden|line-number|ignore-case|fixed-strings|recursive)|\/[SIMOPVNLR]+)$/i.test(
215
+ words[offset2] ?? ""
216
+ ))
217
+ offset2 += 1;
218
+ if (words[offset2] === "--") offset2 += 1;
219
+ const positional = words.slice(offset2);
220
+ const eligible = positional.length > 0 && positional.length <= 2 && !positional.some((word) => word.startsWith("-") || /[$%`<>]/.test(word));
221
+ const target = eligible ? positional[1] ?? "." : JSON.stringify(positional);
222
+ return authorize({ ...denyExploration(operation, target), exceptionEligible: eligible });
223
+ }
224
+ let gitCommand = 1;
225
+ if (operation === "git") {
226
+ while (words[gitCommand]?.startsWith("-")) {
227
+ const option = words[gitCommand];
228
+ gitCommand += ["-C", "-c", "--git-dir", "--work-tree"].includes(option ?? "") ? 2 : 1;
229
+ }
230
+ }
231
+ if (operation === "git" && words[gitCommand]?.toLowerCase() === "grep") {
232
+ const paths = words.indexOf("--");
233
+ const targets = paths >= 0 ? words.slice(paths + 1) : [];
234
+ const eligible = gitCommand === 1 && targets.length <= 1 && (paths >= 0 || words.length === 3) && !targets.some((word) => /[$%`<>]/.test(word));
235
+ return authorize({
236
+ ...denyExploration(
237
+ "git grep",
238
+ eligible ? targets[0] ?? "." : JSON.stringify(words.slice(1))
239
+ ),
240
+ exceptionEligible: eligible
241
+ });
242
+ }
243
+ if (operation === "select-string") {
244
+ const path17 = words.findIndex((word) => /^-(?:literalpath|path)$/i.test(word));
245
+ const targets = [];
246
+ for (let offset2 = path17 + 1; path17 >= 0 && offset2 < words.length; offset2 += 1) {
247
+ const word = words[offset2];
248
+ if (word === void 0 || word.startsWith("-")) break;
249
+ targets.push(word);
250
+ }
251
+ const eligible = targets.length === 1 && !targets.some((word) => /[,$%`<>]/.test(word));
252
+ return authorize({
253
+ ...denyExploration("Select-String", eligible ? targets[0] ?? "." : JSON.stringify(targets)),
254
+ exceptionEligible: eligible
255
+ });
256
+ }
257
+ if (!operation || [
258
+ "echo",
259
+ "printf",
260
+ "write-output",
261
+ "cat",
262
+ "get-content",
263
+ "pnpm",
264
+ "npm",
265
+ "npx",
266
+ "corepack",
267
+ "node",
268
+ "nodejs",
269
+ "python",
270
+ "python3",
271
+ "uv",
272
+ "git",
273
+ "pwd",
274
+ "get-location",
275
+ "ls",
276
+ "dir",
277
+ "sort"
278
+ ].includes(operation)) {
279
+ return void 0;
280
+ }
281
+ return outsideBoundary();
282
+ }
283
+ function outsideBoundary() {
284
+ return {
285
+ decision: "allow",
286
+ state: "outside-boundary",
287
+ message: "Search inside scripts, aliases or continuing input is not verified by this pre-tool boundary."
288
+ };
289
+ }
290
+ function shellPipelines(command, shell) {
291
+ const commands = [];
292
+ let words = [];
293
+ let word = "";
294
+ let quote;
295
+ const flushWord = () => {
296
+ if (word.length) words.push(word);
297
+ word = "";
298
+ };
299
+ for (let index = 0; index < command.length; index += 1) {
300
+ const character = command[index];
301
+ if (character === void 0) continue;
302
+ const next = command[index + 1];
303
+ const escaped = next !== void 0 && (shell === "posix" && character === "\\" && (!quote || quote === '"' && ['"', "\\", "$", "`", "\n"].includes(next)) || shell === "powershell" && character === "`" && quote !== "'" || shell === "cmd" && character === "^" && !quote);
304
+ if (escaped) {
305
+ if (next !== "\n") word += next;
306
+ index += 1;
307
+ continue;
308
+ }
309
+ if (quote) {
310
+ if (character === quote) quote = void 0;
311
+ else word += character;
312
+ } else if (character === '"' || character === "'") {
313
+ quote = character;
314
+ } else if (character === "|" || character === ";" || character === "&" || character === "\n") {
315
+ flushWord();
316
+ if (words.length) commands.push(words);
317
+ words = [];
318
+ } else if (/\s/.test(character)) {
319
+ flushWord();
320
+ } else {
321
+ word += character;
322
+ }
323
+ }
324
+ if (quote) return void 0;
325
+ flushWord();
326
+ if (words.length) commands.push(words);
327
+ return commands;
328
+ }
329
+ function denyExploration(operation, target) {
330
+ const domain = /(?:^|[\\/])(?:docs?|spec|adr|requirements)(?:[\\/]|$)|\.md$/i.test(target) ? "documents" : "code";
331
+ return {
332
+ decision: "deny",
333
+ state: "configured",
334
+ operation,
335
+ target,
336
+ domain,
337
+ 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.`
338
+ };
339
+ }
340
+ function graphFallbackUnavailable(decision, cause, recovery) {
341
+ return {
342
+ ...decision,
343
+ exceptionEligible: false,
344
+ fallbackUnavailable: true,
345
+ 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.`
346
+ };
347
+ }
348
+ function decodeGraphHookEvent(input) {
349
+ if (!object(input)) return void 0;
350
+ const snake = ["tool_name", "tool_input", "session_id"].some((key) => key in input);
351
+ const camel = ["toolName", "toolArgs", "sessionId"].some((key) => key in input);
352
+ if (snake && camel) return void 0;
353
+ const tool = input.tool_name ?? input.toolName;
354
+ let payload = input.tool_input ?? input.toolArgs;
355
+ if (typeof payload === "string") {
356
+ try {
357
+ payload = JSON.parse(payload);
358
+ } catch {
359
+ return void 0;
360
+ }
361
+ }
362
+ if (typeof tool !== "string" || !tool.trim() || !object(payload)) return void 0;
363
+ const session = input.session_id ?? input.sessionId;
364
+ if (session !== void 0 && (typeof session !== "string" || !session.trim()) || input.session_id !== void 0 && input.sessionId !== void 0 && input.session_id !== input.sessionId)
365
+ return void 0;
366
+ const command = payload.command ?? payload.cmd;
367
+ const target = payload.path ?? payload.includePattern;
368
+ const workdir = payload.workdir;
369
+ const shell = payload.shell;
370
+ if (target !== void 0 && (typeof target !== "string" || !target.trim())) return void 0;
371
+ if (workdir !== void 0 && (typeof workdir !== "string" || !workdir.trim())) return void 0;
372
+ if (shell !== void 0 && (typeof shell !== "string" || !shell.trim())) return void 0;
373
+ if (["bash", "powershell", "exec_command"].includes(tool.toLowerCase()) && typeof command !== "string")
374
+ return void 0;
375
+ return {
376
+ tool,
377
+ ...typeof command === "string" ? { command } : {},
378
+ ...typeof target === "string" ? { path: target } : {},
379
+ ...typeof workdir === "string" ? { workdir } : {},
380
+ ...typeof shell === "string" ? { shell } : {},
381
+ ...typeof input.cwd === "string" ? { cwd: input.cwd } : {},
382
+ ...typeof session === "string" ? { session } : {}
383
+ };
384
+ }
385
+ function failedGraphHook() {
386
+ return {
387
+ decision: "deny",
388
+ state: "failed",
389
+ message: "Graph protection failed: invalid pre-tool input. Repair hook configuration before direct exploration."
390
+ };
391
+ }
392
+ function enforceGraphExploration(input, configuration) {
393
+ if (!object(configuration) || Object.keys(configuration).length !== 2 || configuration.kind !== "graph-enforcement" || configuration.denyExploration !== true)
394
+ return failedGraphHook();
395
+ const event = decodeGraphHookEvent(input);
396
+ return event ? classifyGraphExploration(event) : failedGraphHook();
397
+ }
398
+ function graphHookOutput(host, decision) {
399
+ if (decision.decision === "allow") return {};
400
+ return host === "copilot" ? { permissionDecision: "deny", permissionDecisionReason: decision.message } : {
401
+ hookSpecificOutput: {
402
+ hookEventName: "PreToolUse",
403
+ permissionDecision: "deny",
404
+ permissionDecisionReason: decision.message
405
+ }
406
+ };
407
+ }
408
+ function object(value) {
409
+ return typeof value === "object" && value !== null && !Array.isArray(value);
410
+ }
411
+
412
+ // src/core/graph/fallbackState.ts
413
+ var import_node_crypto7 = require("crypto");
414
+ var import_promises13 = require("fs/promises");
415
+ var import_node_path15 = __toESM(require("path"), 1);
416
+
417
+ // src/core/graph/locks.ts
418
+ var import_node_crypto2 = require("crypto");
419
+ var import_promises4 = require("fs/promises");
420
+ var import_node_path5 = __toESM(require("path"), 1);
421
+
422
+ // src/core/graph/layout.ts
423
+ var import_node_path2 = __toESM(require("path"), 1);
424
+ function graphExecutionEnvironment() {
425
+ return { platform: process.platform, architecture: process.arch };
426
+ }
427
+ function graphEnvironmentKey(environment = graphExecutionEnvironment()) {
428
+ if (!/^[a-z0-9]+$/i.test(environment.platform) || !/^[a-z0-9]+$/i.test(environment.architecture))
429
+ throw new Error("Unsupported graph execution environment.");
430
+ return `${environment.platform}-${environment.architecture}`;
431
+ }
432
+
433
+ // src/core/graph/localFiles.ts
434
+ var import_promises3 = require("fs/promises");
435
+ var import_node_path4 = __toESM(require("path"), 1);
436
+
437
+ // src/core/graph/storage.ts
438
+ var import_node_crypto = require("crypto");
439
+ var import_promises2 = require("fs/promises");
440
+ var import_node_path3 = __toESM(require("path"), 1);
441
+ async function assertGraphStoragePath(root, directory) {
442
+ const relative = import_node_path3.default.relative(root, directory);
443
+ const parts = relative.split(import_node_path3.default.sep);
444
+ if (import_node_path3.default.isAbsolute(relative) || parts[0] !== ".qfai" || parts[1] !== "tools" || parts.includes("..")) {
445
+ throw new Error("Graph storage must stay inside project-local .qfai/tools.");
446
+ }
447
+ for (let depth = 1; depth <= parts.length; depth += 1) {
448
+ try {
449
+ const stats = await (0, import_promises2.lstat)(import_node_path3.default.join(root, ...parts.slice(0, depth)));
450
+ if (stats.isSymbolicLink() || !stats.isDirectory())
451
+ throw new Error(
452
+ "Graph storage has an unsafe path component; retain it and repair the project configuration."
453
+ );
454
+ } catch (cause) {
455
+ if (isEnoent(cause)) return;
456
+ throw cause;
457
+ }
458
+ }
459
+ }
460
+
461
+ // src/core/graph/localFiles.ts
462
+ var GRAPH_STATE_BYTES = 32768;
463
+ 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}$/;
464
+ var GraphLocalStateError = class extends Error {
465
+ constructor(message, recovery, options) {
466
+ super(message, options);
467
+ this.recovery = recovery;
468
+ }
469
+ };
470
+ async function graphStateDirectory(root) {
471
+ const directory = import_node_path4.default.join(root, ".qfai", "tools", "state", graphEnvironmentKey());
472
+ await assertGraphStoragePath(root, directory);
473
+ await (0, import_promises3.mkdir)(directory, { recursive: true });
474
+ await assertGraphStoragePath(root, directory);
475
+ return directory;
476
+ }
477
+ function invalidGraphState() {
478
+ return new GraphLocalStateError(
479
+ "Project graph permission state is invalid.",
480
+ "Retain the local state and repair .qfai/tools/state before retrying the graph CLI or hook."
481
+ );
482
+ }
483
+ function graphStateText(value, maximum) {
484
+ return typeof value === "string" && value.trim().length > 0 && value.length <= maximum && !value.includes("\0");
485
+ }
486
+ async function readGraphLocalFile(file) {
487
+ try {
488
+ const before = await (0, import_promises3.lstat)(file);
489
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
490
+ throw invalidGraphState();
491
+ const pinned = await readPinnedRegularFileBytes(file, GRAPH_STATE_BYTES);
492
+ const after = await (0, import_promises3.lstat)(file);
493
+ 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)
494
+ throw invalidGraphState();
495
+ return pinned.content.toString("utf8");
496
+ } catch (cause) {
497
+ if (isEnoent(cause)) return void 0;
498
+ throw cause;
499
+ }
500
+ }
501
+ async function writeGraphLocalFile(file, value) {
502
+ const text = JSON.stringify(value);
503
+ if (Buffer.byteLength(text) > GRAPH_STATE_BYTES) throw invalidGraphState();
504
+ const handle = await (0, import_promises3.open)(file, "wx", 384);
505
+ try {
506
+ await handle.writeFile(text, "utf8");
507
+ } finally {
508
+ await handle.close();
509
+ }
510
+ }
511
+
512
+ // src/core/graph/locks.ts
513
+ async function withGraphLock(root, kind, action) {
514
+ const directory = await graphStateDirectory(root);
515
+ const file = import_node_path5.default.join(directory, `${kind}.lock`);
516
+ const owner = {
517
+ kind: "graph-lock",
518
+ pid: process.pid,
519
+ createdAt: Date.now(),
520
+ token: (0, import_node_crypto2.randomUUID)()
521
+ };
522
+ try {
523
+ await writeGraphLocalFile(file, owner);
524
+ } catch (cause) {
525
+ if (!hasErrnoCode(cause) || cause.code !== "EEXIST") throw cause;
526
+ const text = await readGraphLocalFile(file);
527
+ let active = false;
528
+ try {
529
+ const existing = text === void 0 ? void 0 : JSON.parse(text);
530
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing))
531
+ throw new Error("Invalid graph lock metadata.", { cause });
532
+ const value = existing;
533
+ 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))
534
+ throw new Error("Invalid graph lock metadata.", { cause });
535
+ try {
536
+ process.kill(value.pid, 0);
537
+ active = true;
538
+ } catch (liveness) {
539
+ if (!hasErrnoCode(liveness) || liveness.code !== "ESRCH") active = true;
540
+ }
541
+ } catch {
542
+ }
543
+ throw new GraphLocalStateError(
544
+ `Project graph ${kind} lock ${active ? "is contended" : "requires manual recovery"}.`,
545
+ 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.`,
546
+ { cause }
547
+ );
548
+ }
549
+ try {
550
+ return await action();
551
+ } finally {
552
+ await releaseGraphLock(file, JSON.stringify(owner));
553
+ }
554
+ }
555
+ async function releaseGraphLock(file, owner) {
556
+ const text = await readGraphLocalFile(file);
557
+ if (text !== owner)
558
+ throw new GraphLocalStateError(
559
+ "Project graph lock ownership changed.",
560
+ "Retain the lock and repair the worktree graph state before retrying."
561
+ );
562
+ await (0, import_promises4.unlink)(file);
563
+ }
564
+
565
+ // src/core/graph/permissionRecords.ts
566
+ var import_promises5 = require("fs/promises");
567
+ var import_node_path6 = __toESM(require("path"), 1);
568
+ async function readGraphSearchRecords(root, directory) {
569
+ const records = [];
570
+ const entries = await (0, import_promises5.opendir)(directory);
571
+ for await (const entry of entries) {
572
+ if (entry.name === "state.lock" || entry.name === "analysis.lock") continue;
573
+ if (records.length >= 128) throw fullGraphState();
574
+ const match = /^(.+)\.(pending|grant|draft)\.json$/.exec(entry.name);
575
+ if (!match || !GRAPH_UUID.test(match[1] ?? "")) throw invalidGraphState();
576
+ const file = import_node_path6.default.join(directory, entry.name);
577
+ const text = await readGraphLocalFile(file);
578
+ if (text === void 0) throw invalidGraphState();
579
+ let input;
580
+ try {
581
+ input = JSON.parse(text);
582
+ } catch {
583
+ throw invalidGraphState();
584
+ }
585
+ const record = parseGraphSearchRecord(input, root);
586
+ if (!record || record.id !== match[1] || (match[2] === "pending" ? record.kind !== "pending" : record.kind !== "grant"))
587
+ throw invalidGraphState();
588
+ records.push({ file, record, draft: match[2] === "draft" });
589
+ }
590
+ const current = [];
591
+ for (const item of records) {
592
+ if (Date.now() >= item.record.expiresAt) await (0, import_promises5.unlink)(item.file);
593
+ else current.push(item);
594
+ }
595
+ return current;
596
+ }
597
+ function fullGraphState() {
598
+ return new GraphLocalStateError(
599
+ "Project graph permission state is full.",
600
+ "Retry after pending attempts or search grants expire; retain valid worktree state."
601
+ );
602
+ }
603
+ function parseGraphSearchRecord(input, root) {
604
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
605
+ const value = input;
606
+ const fields = [
607
+ "id",
608
+ "kind",
609
+ "domain",
610
+ "target",
611
+ "operation",
612
+ "worktree",
613
+ "session",
614
+ "createdAt",
615
+ "expiresAt"
616
+ ];
617
+ if (value.kind === "grant") fields.push("reason");
618
+ 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 !== import_node_path6.default.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))
619
+ return void 0;
620
+ const scope = {
621
+ id: value.id,
622
+ domain: value.domain,
623
+ target: value.target,
624
+ operation: value.operation,
625
+ worktree: value.worktree,
626
+ session: value.session,
627
+ createdAt: value.createdAt,
628
+ expiresAt: value.expiresAt
629
+ };
630
+ if (value.kind === "pending") return { ...scope, kind: "pending" };
631
+ return graphStateText(value.reason, 2048) ? { ...scope, kind: "grant", reason: value.reason } : void 0;
632
+ }
633
+
634
+ // src/core/graph/codePreparation.ts
635
+ var import_node_crypto4 = require("crypto");
636
+ var import_promises9 = require("fs/promises");
637
+ var import_node_path11 = __toESM(require("path"), 1);
638
+
639
+ // src/shared/assets.ts
640
+ var import_node_fs2 = require("fs");
641
+ var import_node_path7 = __toESM(require("path"), 1);
642
+ var import_node_url = require("url");
643
+ var INIT_ASSETS_SENTINELS = [
644
+ import_node_path7.default.join(".qfai", "assistant"),
645
+ import_node_path7.default.join("root", "qfai.config.yaml")
646
+ ];
647
+
648
+ // src/core/graph/process.ts
649
+ var import_node_child_process = require("child_process");
650
+ var import_promises6 = require("fs/promises");
651
+ var import_node_path8 = __toESM(require("path"), 1);
652
+ var import_node_util = require("util");
653
+ var execute = (0, import_node_util.promisify)(import_node_child_process.execFile);
654
+
655
+ // src/core/graph/reuse.ts
656
+ var import_node_os = __toESM(require("os"), 1);
657
+ var import_node_path10 = __toESM(require("path"), 1);
658
+ var import_node_crypto3 = require("crypto");
659
+ var import_promises8 = require("fs/promises");
660
+
661
+ // src/core/graph/analysis.ts
662
+ var import_node_path9 = __toESM(require("path"), 1);
663
+ var import_promises7 = require("fs/promises");
664
+
665
+ // src/core/graph/documents.ts
666
+ var import_promises11 = require("fs/promises");
667
+ var import_node_crypto6 = require("crypto");
668
+
669
+ // src/core/graph/documentReuse.ts
670
+ var import_node_crypto5 = require("crypto");
671
+ var import_promises10 = require("fs/promises");
672
+ var import_node_path12 = __toESM(require("path"), 1);
673
+
674
+ // src/core/graph/documents.ts
675
+ var import_node_path13 = __toESM(require("path"), 1);
676
+
677
+ // src/core/graph/query.ts
678
+ var import_promises12 = require("fs/promises");
679
+ var import_node_path14 = __toESM(require("path"), 1);
680
+
681
+ // src/core/graph/fallbackState.ts
682
+ async function classifyStoredGraphExploration(root, event) {
683
+ const decision = classifyGraphExploration(event);
684
+ if (decision.decision !== "deny" || decision.state !== "configured") return decision;
685
+ if (decision.exceptionEligible === false)
686
+ return decision.fallbackUnavailable ? decision : graphFallbackUnavailable(
687
+ decision,
688
+ "The search target or operation cannot be scoped.",
689
+ "Run a single search with one explicit project-relative target, then retry the denied action."
690
+ );
691
+ if (!event.cwd || !graphStateText(event.session, 4096))
692
+ return graphFallbackUnavailable(
693
+ decision,
694
+ "Observed session and worktree are required.",
695
+ "Enable session and cwd in the host event, run qfai init, then retry the denied action from this project worktree."
696
+ );
697
+ if (import_node_path15.default.resolve(event.cwd) !== import_node_path15.default.resolve(root))
698
+ return graphFallbackUnavailable(
699
+ decision,
700
+ "The observed worktree differs from this project worktree.",
701
+ "Run the denied action from this project worktree, or run qfai init in the intended worktree before retrying."
702
+ );
703
+ if (event.workdir && import_node_path15.default.resolve(event.cwd, event.workdir) !== import_node_path15.default.resolve(root))
704
+ return graphFallbackUnavailable(
705
+ decision,
706
+ "The requested workdir differs from this project worktree.",
707
+ "Set workdir to this project worktree and retry the denied action."
708
+ );
709
+ if (!decision.target || !decision.operation || !decision.domain) return decision;
710
+ const worktree = import_node_path15.default.resolve(root);
711
+ const session = event.session;
712
+ return withGraphLock(root, "state", async () => {
713
+ const directory = await graphStateDirectory(root);
714
+ const records = await readGraphSearchRecords(root, directory);
715
+ const classified = classifyGraphExploration(event, (candidate) => {
716
+ if (candidate.decision !== "deny" || candidate.exceptionEligible === false) return candidate;
717
+ const grant = records.find(
718
+ (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
719
+ );
720
+ return grant?.record.kind === "grant" ? {
721
+ ...candidate,
722
+ decision: "allow",
723
+ message: `AI-decided search fallback: ${grant.record.reason}`
724
+ } : candidate;
725
+ });
726
+ if (classified.decision !== "deny" || classified.state !== "configured" || classified.exceptionEligible === false || !classified.target || !classified.operation || !classified.domain)
727
+ return classified;
728
+ const observed = {
729
+ target: classified.target,
730
+ operation: classified.operation,
731
+ domain: classified.domain,
732
+ worktree,
733
+ session
734
+ };
735
+ if (records.length >= 128) throw fullGraphState();
736
+ const createdAt = Date.now();
737
+ const pending = {
738
+ ...observed,
739
+ id: (0, import_node_crypto7.randomUUID)(),
740
+ kind: "pending",
741
+ createdAt,
742
+ expiresAt: createdAt + 6e5
743
+ };
744
+ await writeGraphLocalFile(import_node_path15.default.join(directory, `${pending.id}.pending.json`), pending);
745
+ return { ...classified, message: `${classified.message} Observed attempt: ${pending.id}.` };
746
+ });
747
+ }
748
+
749
+ // src/cli/graphHook.ts
750
+ function hostName(value) {
751
+ return value === "claude" || value === "codex" || value === "copilot" || value === "vscode";
752
+ }
753
+ async function runInstalledGraphHook(root, args, io = {
754
+ read: () => JSON.parse((0, import_node_fs3.readFileSync)(0, "utf8")),
755
+ output: (text) => {
756
+ process.stdout.write(text);
757
+ },
758
+ error: (text) => {
759
+ process.stderr.write(text);
760
+ },
761
+ fail: () => {
762
+ process.exitCode = 2;
763
+ }
764
+ }) {
765
+ let input;
766
+ try {
767
+ input = io.read();
768
+ } catch {
769
+ input = void 0;
770
+ }
771
+ const host = args[0] === "shared" ? wireHost(input) : args[0];
772
+ if (!hostName(host)) {
773
+ io.error("Graph protection unsupported: select a supported host adapter.\n");
774
+ io.fail();
775
+ return;
776
+ }
777
+ let decision = failedGraphHook();
778
+ try {
779
+ const components = [".qfai", "tools", "graph-enforcement.json"];
780
+ if (!await findUnsafeHostFileComponent(root, components)) {
781
+ const text = await readPinnedRegularFile(import_node_path16.default.join(root, ...components), 4096);
782
+ if (text !== null) decision = enforceGraphExploration(input, JSON.parse(text));
783
+ }
784
+ } catch {
785
+ }
786
+ if (decision.decision === "deny" && decision.state === "configured") {
787
+ const event = decodeGraphHookEvent(input);
788
+ if (event) {
789
+ try {
790
+ decision = await classifyStoredGraphExploration(root, event);
791
+ } catch (cause) {
792
+ decision = failedGraphHook();
793
+ if (cause instanceof GraphLocalStateError)
794
+ decision.message = `${cause.message} ${cause.recovery}`;
795
+ }
796
+ }
797
+ }
798
+ if (decision.decision === "deny")
799
+ io.output(JSON.stringify(graphHookOutput(host, decision)) + "\n");
800
+ if (decision.state === "failed") {
801
+ io.error("Graph protection failed. Repair the project hook and enforcement configuration.\n");
802
+ io.fail();
803
+ }
804
+ }
805
+ function wireHost(input) {
806
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
807
+ const value = input;
808
+ const snake = ["tool_name", "tool_input", "session_id"].some((key) => key in value);
809
+ const camel = ["toolName", "toolArgs", "sessionId"].some((key) => key in value);
810
+ if (snake === camel) return void 0;
811
+ return camel ? "copilot" : "vscode";
812
+ }
813
+ // Annotate the CommonJS export names for ESM import in node:
814
+ 0 && (module.exports = {
815
+ runInstalledGraphHook
816
+ });
817
+ //# sourceMappingURL=hook.cjs.map