@interf/compiler 0.33.0 → 0.50.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 (234) hide show
  1. package/README.md +122 -226
  2. package/dist/cli/commands/agents.js +1 -32
  3. package/dist/cli/commands/benchmark.d.ts +2 -3
  4. package/dist/cli/commands/benchmark.js +1 -31
  5. package/dist/cli/commands/build-plan.js +26 -50
  6. package/dist/cli/commands/build.d.ts +2 -3
  7. package/dist/cli/commands/build.js +1 -31
  8. package/dist/cli/commands/graphs.js +177 -32
  9. package/dist/cli/commands/mcp.d.ts +1 -0
  10. package/dist/cli/commands/mcp.js +223 -126
  11. package/dist/cli/commands/project.js +10 -36
  12. package/dist/cli/commands/reset.d.ts +2 -3
  13. package/dist/cli/commands/reset.js +1 -22
  14. package/dist/cli/commands/runs.js +86 -33
  15. package/dist/cli/commands/status.js +3 -24
  16. package/dist/cli/commands/traces.js +1 -29
  17. package/dist/cli/commands/wizard.js +17 -29
  18. package/dist/cli/lib/http-client.d.ts +39 -0
  19. package/dist/cli/lib/http-client.js +73 -0
  20. package/dist/packages/build-plans/authoring/brief.d.ts +25 -4
  21. package/dist/packages/build-plans/authoring/build-plan-authoring.d.ts +42 -1
  22. package/dist/packages/build-plans/authoring/build-plan-authoring.js +470 -63
  23. package/dist/packages/build-plans/authoring/build-plan-edit-session.d.ts +9 -0
  24. package/dist/packages/build-plans/authoring/build-plan-edit-session.js +27 -10
  25. package/dist/packages/build-plans/authoring/build-plan-improvement.js +62 -8
  26. package/dist/packages/build-plans/authoring/lib/build-plan-edit-utils.d.ts +1 -0
  27. package/dist/packages/build-plans/package/build-plan-definitions.d.ts +0 -1
  28. package/dist/packages/build-plans/package/build-plan-definitions.js +5 -3
  29. package/dist/packages/build-plans/package/build-plan-stage-runner.d.ts +1 -0
  30. package/dist/packages/build-plans/package/build-plan-stage-runner.js +2 -1
  31. package/dist/packages/build-plans/package/builtin-build-plan.d.ts +2 -2
  32. package/dist/packages/build-plans/package/builtin-build-plan.js +3 -3
  33. package/dist/packages/build-plans/package/context-interface.d.ts +3 -0
  34. package/dist/packages/build-plans/package/context-interface.js +5 -5
  35. package/dist/packages/build-plans/package/interf-build-plan-package.js +22 -22
  36. package/dist/packages/build-plans/package/local-build-plans.d.ts +10 -5
  37. package/dist/packages/build-plans/package/local-build-plans.js +57 -32
  38. package/dist/packages/contracts/index.d.ts +4 -3
  39. package/dist/packages/contracts/index.js +2 -1
  40. package/dist/packages/contracts/lib/context-graph-layer.d.ts +161 -0
  41. package/dist/packages/contracts/lib/context-graph-layer.js +216 -0
  42. package/dist/packages/contracts/lib/project-paths.d.ts +7 -0
  43. package/dist/packages/contracts/lib/project-paths.js +9 -0
  44. package/dist/packages/contracts/lib/project-schema.d.ts +264 -1
  45. package/dist/packages/contracts/lib/project-schema.js +38 -13
  46. package/dist/packages/contracts/lib/schema.d.ts +556 -23
  47. package/dist/packages/contracts/lib/schema.js +279 -18
  48. package/dist/packages/contracts/utils/filesystem.d.ts +1 -0
  49. package/dist/packages/contracts/utils/filesystem.js +29 -1
  50. package/dist/packages/projects/lib/schema.d.ts +6 -8
  51. package/dist/packages/projects/lib/schema.js +3 -1
  52. package/dist/packages/projects/source-config.d.ts +0 -5
  53. package/dist/packages/projects/source-config.js +9 -22
  54. package/dist/packages/runtime/actions/fields.d.ts +4 -0
  55. package/dist/packages/runtime/actions/form-builders.js +79 -31
  56. package/dist/packages/runtime/actions/form-validators.js +9 -3
  57. package/dist/packages/runtime/actions/helpers.js +3 -3
  58. package/dist/packages/runtime/actions/registry.d.ts +1 -1
  59. package/dist/packages/runtime/actions/registry.js +1 -1
  60. package/dist/packages/runtime/actions/requests.d.ts +1 -1
  61. package/dist/packages/runtime/actions/requests.js +12 -6
  62. package/dist/packages/runtime/actions/schemas.d.ts +7 -0
  63. package/dist/packages/runtime/actions/schemas.js +1 -0
  64. package/dist/packages/runtime/agent-handoff.js +8 -7
  65. package/dist/packages/runtime/agents/lib/execution-profile.d.ts +14 -0
  66. package/dist/packages/runtime/agents/lib/execution-profile.js +23 -0
  67. package/dist/packages/runtime/agents/lib/execution.js +14 -8
  68. package/dist/packages/runtime/agents/lib/executors.d.ts +1 -0
  69. package/dist/packages/runtime/agents/lib/executors.js +11 -2
  70. package/dist/packages/runtime/agents/lib/logs.d.ts +10 -0
  71. package/dist/packages/runtime/agents/lib/logs.js +32 -8
  72. package/dist/packages/runtime/agents/lib/preflight.js +4 -1
  73. package/dist/packages/runtime/agents/lib/render.d.ts +18 -0
  74. package/dist/packages/runtime/agents/lib/render.js +44 -18
  75. package/dist/packages/runtime/agents/lib/shell-templates.js +105 -63
  76. package/dist/packages/runtime/agents/lib/shells.d.ts +29 -0
  77. package/dist/packages/runtime/agents/lib/shells.js +158 -32
  78. package/dist/packages/runtime/agents/lib/source-context-scan.d.ts +10 -0
  79. package/dist/packages/runtime/agents/lib/source-context-scan.js +388 -0
  80. package/dist/packages/runtime/agents/lib/status.js +1 -14
  81. package/dist/packages/runtime/agents/lib/string-utils.d.ts +16 -0
  82. package/dist/packages/runtime/agents/lib/string-utils.js +36 -0
  83. package/dist/packages/runtime/agents/lib/types.d.ts +1 -0
  84. package/dist/packages/runtime/agents/providers/codex.js +2 -0
  85. package/dist/packages/runtime/agents/role-executors.js +2 -1
  86. package/dist/packages/runtime/auth/session-store.js +11 -3
  87. package/dist/packages/runtime/benchmark-question-draft.d.ts +3 -0
  88. package/dist/packages/runtime/benchmark-question-draft.js +57 -28
  89. package/dist/packages/runtime/build/artifact-status.d.ts +1 -1
  90. package/dist/packages/runtime/build/artifact-status.js +1 -1
  91. package/dist/packages/runtime/build/build-evidence.d.ts +2 -1
  92. package/dist/packages/runtime/build/build-evidence.js +11 -5
  93. package/dist/packages/runtime/build/build-pipeline.js +89 -5
  94. package/dist/packages/runtime/build/build-stage-plan.js +3 -1
  95. package/dist/packages/runtime/build/build-stage-runner.js +169 -32
  96. package/dist/packages/runtime/build/build-target.d.ts +3 -0
  97. package/dist/packages/runtime/build/build-target.js +25 -1
  98. package/dist/packages/runtime/build/check-evaluator.d.ts +1 -1
  99. package/dist/packages/runtime/build/check-evaluator.js +655 -4
  100. package/dist/packages/runtime/build/context-graph-paths.d.ts +13 -0
  101. package/dist/packages/runtime/build/context-graph-paths.js +27 -0
  102. package/dist/packages/runtime/build/index.d.ts +2 -2
  103. package/dist/packages/runtime/build/index.js +2 -2
  104. package/dist/packages/runtime/build/inspect-map.d.ts +10 -0
  105. package/dist/packages/runtime/build/inspect-map.js +270 -0
  106. package/dist/packages/runtime/build/lib/schema.d.ts +246 -53
  107. package/dist/packages/runtime/build/lib/schema.js +173 -15
  108. package/dist/packages/runtime/build/native-entrypoint.d.ts +2 -0
  109. package/dist/packages/runtime/build/native-entrypoint.js +286 -0
  110. package/dist/packages/runtime/build/runtime-contracts.js +9 -3
  111. package/dist/packages/runtime/build/runtime-log-paths.d.ts +3 -0
  112. package/dist/packages/runtime/build/runtime-log-paths.js +16 -0
  113. package/dist/packages/runtime/build/runtime-prompt.js +6 -4
  114. package/dist/packages/runtime/build/runtime-runs.js +63 -10
  115. package/dist/packages/runtime/build/runtime-types.d.ts +4 -1
  116. package/dist/packages/runtime/build/runtime.d.ts +3 -1
  117. package/dist/packages/runtime/build/runtime.js +3 -1
  118. package/dist/packages/runtime/build/source-files.js +11 -2
  119. package/dist/packages/runtime/build/source-inventory.d.ts +1 -0
  120. package/dist/packages/runtime/build/source-inventory.js +246 -7
  121. package/dist/packages/runtime/build/source-manifest.d.ts +11 -0
  122. package/dist/packages/runtime/build/source-manifest.js +30 -2
  123. package/dist/packages/runtime/build/stage-evidence.js +80 -11
  124. package/dist/packages/runtime/build/stage-manifest.d.ts +45 -0
  125. package/dist/packages/runtime/build/stage-manifest.js +1125 -0
  126. package/dist/packages/runtime/build/stage-reuse.js +12 -0
  127. package/dist/packages/runtime/build/stage-session.d.ts +81 -0
  128. package/dist/packages/runtime/build/stage-session.js +308 -0
  129. package/dist/packages/runtime/build/state-io.js +10 -11
  130. package/dist/packages/runtime/build/state-view.js +1 -1
  131. package/dist/packages/runtime/build/state.d.ts +1 -1
  132. package/dist/packages/runtime/build/state.js +1 -1
  133. package/dist/packages/runtime/build/summary-coverage-index.d.ts +21 -0
  134. package/dist/packages/runtime/build/summary-coverage-index.js +189 -0
  135. package/dist/packages/runtime/build/traces.js +3 -3
  136. package/dist/packages/runtime/build/validate-context-graph.d.ts +1 -1
  137. package/dist/packages/runtime/build/validate-context-graph.js +5 -5
  138. package/dist/packages/runtime/build/validate.d.ts +1 -1
  139. package/dist/packages/runtime/build/validate.js +1 -1
  140. package/dist/packages/runtime/client.d.ts +3 -3
  141. package/dist/packages/runtime/client.js +8 -13
  142. package/dist/packages/runtime/context-checks.js +13 -0
  143. package/dist/packages/runtime/context-graph-scaffold.js +2 -1
  144. package/dist/packages/runtime/context-graph-semantic-graph.d.ts +9 -0
  145. package/dist/packages/runtime/context-graph-semantic-graph.js +416 -0
  146. package/dist/packages/runtime/execution/lib/schema.d.ts +34 -31
  147. package/dist/packages/runtime/index.d.ts +2 -2
  148. package/dist/packages/runtime/index.js +1 -1
  149. package/dist/packages/runtime/native-run-handlers.d.ts +38 -0
  150. package/dist/packages/runtime/native-run-handlers.js +52 -33
  151. package/dist/packages/runtime/plan-artifact-contract.js +1 -1
  152. package/dist/packages/runtime/project-source-state.d.ts +4 -4
  153. package/dist/packages/runtime/project-source-state.js +5 -2
  154. package/dist/packages/runtime/project-store.d.ts +5 -0
  155. package/dist/packages/runtime/project-store.js +30 -3
  156. package/dist/packages/runtime/requested-artifacts.js +1 -1
  157. package/dist/packages/runtime/run-observability.js +9 -4
  158. package/dist/packages/runtime/runtime-action-proposals.js +3 -3
  159. package/dist/packages/runtime/runtime-build-plans.js +47 -3
  160. package/dist/packages/runtime/runtime-build-runs.js +9 -16
  161. package/dist/packages/runtime/runtime-caches.d.ts +26 -0
  162. package/dist/packages/runtime/runtime-caches.js +47 -0
  163. package/dist/packages/runtime/runtime-jobs.js +6 -6
  164. package/dist/packages/runtime/runtime-project-mutations.js +1 -0
  165. package/dist/packages/runtime/runtime-project-reads.d.ts +4 -1
  166. package/dist/packages/runtime/runtime-project-reads.js +229 -36
  167. package/dist/packages/runtime/runtime-proposal-helpers.js +6 -6
  168. package/dist/packages/runtime/runtime-resource-builders.d.ts +4 -2
  169. package/dist/packages/runtime/runtime-resource-builders.js +16 -14
  170. package/dist/packages/runtime/runtime-status.d.ts +14 -0
  171. package/dist/packages/runtime/runtime-status.js +15 -0
  172. package/dist/packages/runtime/runtime-verify-runs.js +6 -5
  173. package/dist/packages/runtime/runtime.d.ts +439 -22
  174. package/dist/packages/runtime/runtime.js +16 -2
  175. package/dist/packages/runtime/schemas/actions.d.ts +24 -0
  176. package/dist/packages/runtime/schemas/agents.d.ts +28 -0
  177. package/dist/packages/runtime/schemas/agents.js +33 -0
  178. package/dist/packages/runtime/schemas/build-plans.d.ts +181 -8
  179. package/dist/packages/runtime/schemas/build-plans.js +36 -2
  180. package/dist/packages/runtime/schemas/context-graphs.d.ts +1522 -0
  181. package/dist/packages/runtime/schemas/context-graphs.js +110 -0
  182. package/dist/packages/runtime/schemas/files.d.ts +7 -347
  183. package/dist/packages/runtime/schemas/files.js +1 -24
  184. package/dist/packages/runtime/schemas/index.d.ts +1 -0
  185. package/dist/packages/runtime/schemas/index.js +1 -0
  186. package/dist/packages/runtime/schemas/jobs.js +4 -0
  187. package/dist/packages/runtime/schemas/projects.d.ts +48 -21
  188. package/dist/packages/runtime/schemas/projects.js +34 -10
  189. package/dist/packages/runtime/schemas/runs.d.ts +1009 -240
  190. package/dist/packages/runtime/schemas/runs.js +17 -0
  191. package/dist/packages/runtime/service/openapi.js +1 -0
  192. package/dist/packages/runtime/service/operations.d.ts +1666 -145
  193. package/dist/packages/runtime/service/operations.js +147 -17
  194. package/dist/packages/runtime/service/routes.d.ts +11 -3
  195. package/dist/packages/runtime/service/routes.js +11 -3
  196. package/dist/packages/runtime/service/server-app-boot.js +2 -2
  197. package/dist/packages/runtime/service/server-helpers.d.ts +11 -0
  198. package/dist/packages/runtime/service/server-helpers.js +19 -0
  199. package/dist/packages/runtime/service/server-routes-action-proposals.js +4 -2
  200. package/dist/packages/runtime/service/server-routes-agents.js +19 -85
  201. package/dist/packages/runtime/service/server-routes-build-plans.js +14 -11
  202. package/dist/packages/runtime/service/server-routes-project-context.js +102 -7
  203. package/dist/packages/runtime/service/server-routes-project-jobs.js +19 -12
  204. package/dist/packages/runtime/service/server-routes-project-runs.js +5 -2
  205. package/dist/packages/runtime/service/server-routes-projects.js +6 -2
  206. package/dist/packages/runtime/service/server-routes-runs.js +11 -4
  207. package/dist/packages/runtime/verify/lib/schema.js +12 -0
  208. package/dist/packages/runtime/verify/test-file-guard.d.ts +2 -0
  209. package/dist/packages/runtime/verify/test-file-guard.js +29 -0
  210. package/dist/packages/runtime/verify/verify-execution.d.ts +7 -0
  211. package/dist/packages/runtime/verify/verify-execution.js +109 -35
  212. package/dist/packages/runtime/verify/verify-paths.d.ts +1 -0
  213. package/dist/packages/runtime/verify/verify-paths.js +4 -0
  214. package/dist/packages/runtime/verify/verify-specs.js +49 -39
  215. package/dist/packages/runtime/wire-schemas.d.ts +1 -1
  216. package/dist/packages/runtime/wire-schemas.js +1 -1
  217. package/package.json +2 -8
  218. package/public-repo/CONTRIBUTING.md +10 -3
  219. package/public-repo/README.md +122 -226
  220. package/public-repo/build-plans/interf-default/README.md +15 -12
  221. package/public-repo/build-plans/interf-default/build/stages/entrypoint/SKILL.md +74 -0
  222. package/public-repo/build-plans/interf-default/build/stages/knowledge/SKILL.md +95 -0
  223. package/public-repo/build-plans/interf-default/build/stages/summarize/SKILL.md +38 -5
  224. package/public-repo/build-plans/interf-default/build-plan.json +27 -23
  225. package/public-repo/build-plans/interf-default/build-plan.schema.json +24 -20
  226. package/public-repo/build-plans/interf-default/use/query/SKILL.md +8 -7
  227. package/public-repo/openapi/local-service.openapi.json +11637 -4213
  228. package/public-repo/skills/interf/SKILL.md +174 -134
  229. package/dist/packages/runtime/build/runtime-paths.d.ts +0 -8
  230. package/dist/packages/runtime/build/runtime-paths.js +0 -26
  231. package/dist/packages/runtime/build/state-paths.d.ts +0 -7
  232. package/dist/packages/runtime/build/state-paths.js +0 -22
  233. package/public-repo/build-plans/interf-default/build/stages/shape/SKILL.md +0 -34
  234. package/public-repo/build-plans/interf-default/build/stages/structure/SKILL.md +0 -28
@@ -0,0 +1,388 @@
1
+ import { opendirSync, realpathSync, statSync } from "node:fs";
2
+ import { extname, join } from "node:path";
3
+ /**
4
+ * Cheap, contents-free Source scan for Build Plan authoring.
5
+ *
6
+ * The Build Plan authoring agent locks a plan skeleton before reading any
7
+ * Source file. Without an inventory it defaults to a generic 4-stage plan
8
+ * for every Source. This scan gives the draft agent a real, task-agnostic
9
+ * map of the Source up front so it can shape stages to what is actually
10
+ * there.
11
+ *
12
+ * Guarantees:
13
+ * - Never opens or reads file contents. It only lists directory entries and
14
+ * parses names/extensions. Source files stay read-only and unread.
15
+ * - Generic: no intent keywords, no task taxonomy, no per-Project answers.
16
+ * It works for a folder of calls, PDFs, a codebase, or anything else.
17
+ * - O(file-listing) with hard caps so a pathological tree (deep nesting,
18
+ * huge fan-out) can never turn the scan into a recursion or time bomb.
19
+ */
20
+ const MAX_ENTRIES = 20000;
21
+ const MAX_DEPTH = 6;
22
+ const MAX_FOLDER_ITEMS = 40;
23
+ const MAX_SAMPLE_NAMES = 6;
24
+ const MAX_OBSERVATIONS = 24;
25
+ const IGNORED_ENTRY_NAMES = new Set([
26
+ ".git",
27
+ ".interf",
28
+ "node_modules",
29
+ ".DS_Store",
30
+ ".cache",
31
+ "__pycache__",
32
+ ".venv",
33
+ ".svn",
34
+ ".hg",
35
+ ]);
36
+ // Canonical, task-agnostic file kinds. Mirrors the Source Manifest kind
37
+ // vocabulary (pdf, document, spreadsheet, presentation, image, text, other)
38
+ // plus coarse buckets that are useful for plan shaping (code, data, audio,
39
+ // video, archive). No kind is intent-specific.
40
+ function classifyKind(extension) {
41
+ const ext = extension.replace(/^\./, "").toLowerCase();
42
+ if (!ext)
43
+ return "no-extension";
44
+ if (ext === "pdf")
45
+ return "pdf";
46
+ if (["doc", "docx", "rtf", "odt", "pages"].includes(ext))
47
+ return "document";
48
+ if (["csv", "tsv", "xls", "xlsx", "ods", "numbers", "parquet"].includes(ext)) {
49
+ return "spreadsheet";
50
+ }
51
+ if (["ppt", "pptx", "odp", "key"].includes(ext))
52
+ return "presentation";
53
+ if (["jpg", "jpeg", "png", "gif", "webp", "svg", "heic", "tiff", "bmp"].includes(ext)) {
54
+ return "image";
55
+ }
56
+ if (["mp3", "wav", "m4a", "aac", "flac", "ogg", "aiff"].includes(ext))
57
+ return "audio";
58
+ if (["mp4", "mov", "avi", "mkv", "webm", "m4v"].includes(ext))
59
+ return "video";
60
+ if (["zip", "tar", "gz", "tgz", "bz2", "7z", "rar"].includes(ext))
61
+ return "archive";
62
+ if (["md", "markdown", "txt", "text"].includes(ext))
63
+ return "text";
64
+ if (["json", "ndjson", "yaml", "yml", "xml", "toml", "ini"].includes(ext))
65
+ return "data";
66
+ if ([
67
+ "ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "rb", "go", "rs", "java",
68
+ "kt", "c", "h", "cpp", "hpp", "cs", "php", "swift", "sh", "sql", "html",
69
+ "css", "scss", "vue", "svelte",
70
+ ].includes(ext)) {
71
+ return "code";
72
+ }
73
+ return "other";
74
+ }
75
+ // Bare year (2019), year-month (2024-03, 2024_03), or full date
76
+ // (2024-03-15, 20240315) appearing in a filename. Filename-only; we never
77
+ // read file contents to find dates.
78
+ const DATE_TOKEN = /\b(19|20)\d{2}([-_./]?(0[1-9]|1[0-2]))?([-_./]?(0[1-9]|[12]\d|3[01]))?\b/g;
79
+ function extractDateTokens(name) {
80
+ const matches = name.match(DATE_TOKEN);
81
+ if (!matches)
82
+ return [];
83
+ return matches.filter((token) => /^(19|20)\d{2}/.test(token));
84
+ }
85
+ function makeFolder() {
86
+ return { fileCount: 0, kinds: new Map(), sampleNames: [] };
87
+ }
88
+ function recordFile(folder, name) {
89
+ folder.fileCount += 1;
90
+ const kind = classifyKind(extname(name));
91
+ folder.kinds.set(kind, (folder.kinds.get(kind) ?? 0) + 1);
92
+ if (folder.sampleNames.length < MAX_SAMPLE_NAMES) {
93
+ folder.sampleNames.push(name);
94
+ }
95
+ }
96
+ function kindSummary(kinds) {
97
+ return [...kinds.entries()]
98
+ .sort((a, b) => b[1] - a[1])
99
+ .map(([kind, count]) => `${count} ${kind}`)
100
+ .join(", ");
101
+ }
102
+ /**
103
+ * Resolve a directory entry to a coarse type, following symlinks.
104
+ *
105
+ * `Dirent` reports the type of the entry itself, so a symlink is neither a
106
+ * file nor a directory and would be dropped. The Source can legitimately be
107
+ * (or contain) symlinked files and dirs — mirroring the repo walker in
108
+ * contracts/utils/filesystem.ts, we stat the link target and classify the
109
+ * entry as the target's type. Broken or inaccessible links resolve to
110
+ * "other" and are skipped. We never read file contents here.
111
+ */
112
+ function resolveEntryType(fullPath, isSymlink, isDir, isFile) {
113
+ if (isDir)
114
+ return { type: "dir", realDir: null };
115
+ if (isFile)
116
+ return { type: "file", realDir: null };
117
+ if (!isSymlink)
118
+ return { type: "other", realDir: null };
119
+ // Symlink: stat (follows the link) to learn the target's real type.
120
+ try {
121
+ const target = statSync(fullPath);
122
+ if (target.isDirectory()) {
123
+ // Need the canonical target path to guard against symlink cycles.
124
+ let realDir = null;
125
+ try {
126
+ realDir = realpathSync(fullPath);
127
+ }
128
+ catch {
129
+ realDir = null;
130
+ }
131
+ return { type: "dir", realDir };
132
+ }
133
+ if (target.isFile())
134
+ return { type: "file", realDir: null };
135
+ return { type: "other", realDir: null };
136
+ }
137
+ catch {
138
+ // Broken or inaccessible link.
139
+ return { type: "other", realDir: null };
140
+ }
141
+ }
142
+ /**
143
+ * Walk the Source tree shallowly and group counts by the top-level folder
144
+ * (files directly under the root group under "."). Bounded by MAX_ENTRIES
145
+ * and MAX_DEPTH so the cost is always O(file-listing) with a ceiling.
146
+ *
147
+ * Directories are streamed via `opendirSync` and read one entry at a time so
148
+ * a pathological directory with hundreds of thousands of entries can never be
149
+ * materialized into a single array — the MAX_ENTRIES ceiling is enforced
150
+ * while iterating, and iteration stops the moment it is hit. Symlinked files
151
+ * and directories are followed (with cycle protection) so a Source built from
152
+ * symlinks is counted instead of silently yielding an empty inventory.
153
+ */
154
+ function scan(sourceFolderPath) {
155
+ const folders = new Map();
156
+ const totalKinds = new Map();
157
+ const dateTokens = [];
158
+ let totalFiles = 0;
159
+ let visited = 0;
160
+ let truncated = false;
161
+ // A read failure on the Source root itself (permission denied, path is a
162
+ // file, etc.) means a real Source may exist but Interf could not enumerate
163
+ // it. That is distinct from an empty-but-readable Source and must not be
164
+ // silently flattened into the same "no inventory" result. ENOENT (the root
165
+ // is simply not there) is treated as empty, not as a read failure.
166
+ let rootReadError = null;
167
+ // Canonical paths of directories already walked. Following symlinked dirs
168
+ // can introduce cycles; this set keeps the walk finite. Seeded lazily with
169
+ // each directory's realpath as it is opened.
170
+ const visitedDirs = new Set();
171
+ // Stack-based shallow walk. Each entry carries the absolute dir path, its
172
+ // depth, and the top-level group it rolls up into.
173
+ const stack = [
174
+ { dir: sourceFolderPath, depth: 0, group: "." },
175
+ ];
176
+ while (stack.length > 0) {
177
+ if (visited >= MAX_ENTRIES) {
178
+ truncated = true;
179
+ break;
180
+ }
181
+ const { dir, depth, group } = stack.pop();
182
+ // Cycle guard: skip a directory whose canonical path we have already
183
+ // walked (reached again through a symlink).
184
+ let canonicalDir;
185
+ try {
186
+ canonicalDir = realpathSync(dir);
187
+ }
188
+ catch {
189
+ canonicalDir = dir;
190
+ }
191
+ if (visitedDirs.has(canonicalDir))
192
+ continue;
193
+ visitedDirs.add(canonicalDir);
194
+ // Stream the directory: opendirSync yields one Dirent at a time so a
195
+ // directory with 100k+ entries is never materialized as an array. We
196
+ // stop reading as soon as the MAX_ENTRIES budget is exhausted.
197
+ let handle;
198
+ try {
199
+ handle = opendirSync(dir);
200
+ }
201
+ catch (error) {
202
+ // A failure to open the Source root is a read failure we must surface,
203
+ // not swallow — otherwise an unreadable Source produces the same empty
204
+ // inventory as a genuinely empty one and the draft silently goes
205
+ // generic. ENOENT (root absent) is left as the empty/null case so an
206
+ // unbound or missing Source still degrades quietly. Failures on nested
207
+ // directories during the walk stay tolerated: they yield a partial
208
+ // inventory, which the truncation note already covers.
209
+ if (depth === 0) {
210
+ const errno = error;
211
+ if (errno?.code !== "ENOENT")
212
+ rootReadError = errno;
213
+ }
214
+ continue;
215
+ }
216
+ try {
217
+ let entry = handle.readSync();
218
+ while (entry !== null) {
219
+ if (visited >= MAX_ENTRIES) {
220
+ truncated = true;
221
+ break;
222
+ }
223
+ const name = entry.name;
224
+ if (IGNORED_ENTRY_NAMES.has(name) || name.startsWith(".")) {
225
+ entry = handle.readSync();
226
+ continue;
227
+ }
228
+ visited += 1;
229
+ const resolved = resolveEntryType(join(dir, name), entry.isSymbolicLink(), entry.isDirectory(), entry.isFile());
230
+ if (resolved.type === "dir") {
231
+ // At the root, each subfolder becomes its own top-level group.
232
+ const childGroup = depth === 0 ? name : group;
233
+ if (!folders.has(childGroup))
234
+ folders.set(childGroup, makeFolder());
235
+ if (depth + 1 <= MAX_DEPTH) {
236
+ if (!(resolved.realDir !== null && visitedDirs.has(resolved.realDir))) {
237
+ stack.push({ dir: join(dir, name), depth: depth + 1, group: childGroup });
238
+ }
239
+ }
240
+ else {
241
+ truncated = true;
242
+ }
243
+ }
244
+ else if (resolved.type === "file") {
245
+ const folder = folders.get(group) ?? makeFolder();
246
+ if (!folders.has(group))
247
+ folders.set(group, folder);
248
+ recordFile(folder, name);
249
+ totalFiles += 1;
250
+ const kind = classifyKind(extname(name));
251
+ totalKinds.set(kind, (totalKinds.get(kind) ?? 0) + 1);
252
+ for (const token of extractDateTokens(name))
253
+ dateTokens.push(token);
254
+ }
255
+ entry = handle.readSync();
256
+ }
257
+ }
258
+ finally {
259
+ handle.closeSync();
260
+ }
261
+ }
262
+ return { folders, totalFiles, totalKinds, dateTokens, truncated, rootReadError };
263
+ }
264
+ function dateRange(tokens) {
265
+ if (tokens.length === 0)
266
+ return null;
267
+ // Compare on a normalized digits-only key so 2024-03 sorts against
268
+ // 20240315 consistently; report the original tokens for the bounds.
269
+ // Drop any token whose normalized key is not a YYYY[MMDD] digit key — a
270
+ // malformed or separator-only token must never become a reported date
271
+ // bound, which would feed the draft a misleading "Filenames span X to Y".
272
+ const keyed = tokens
273
+ .map((token) => ({
274
+ token,
275
+ key: token.replace(/[-_./]/g, "").padEnd(8, "0"),
276
+ }))
277
+ .filter(({ key }) => /^(19|20)\d{6}$/.test(key));
278
+ if (keyed.length === 0)
279
+ return null;
280
+ keyed.sort((a, b) => a.key.localeCompare(b.key));
281
+ return { min: keyed[0].token, max: keyed[keyed.length - 1].token };
282
+ }
283
+ /**
284
+ * Build the SourceContext returned when the Source root could not be read.
285
+ *
286
+ * A read failure is NOT the empty/null case: returning null here would let the
287
+ * draft go generic as if the Source had no files, hiding a real Source that
288
+ * Interf simply could not enumerate (permission denied, path is a file, etc.).
289
+ * Instead we surface a populated SourceContext whose limitations make the
290
+ * failure explicit, so the authoring agent sees it and does not lock a generic
291
+ * plan on a false "empty Source" reading. No file is ever opened or read.
292
+ */
293
+ function readErrorContext(sourceFolderPath, error) {
294
+ const code = error?.code ? ` (${error.code})` : "";
295
+ return {
296
+ summary: "Contents-free Source scan could not read the Source root" +
297
+ `${code}; no inventory was produced.`,
298
+ items: [],
299
+ observations: [
300
+ `Interf could not enumerate the Source at ${sourceFolderPath}${code}.`,
301
+ ],
302
+ limitations: [
303
+ "Interf could not read the Source root, so this is NOT an empty Source. " +
304
+ "Do not lock a generic plan: inspect the Source through your own tools " +
305
+ "and confirm access before shaping stages.",
306
+ ],
307
+ };
308
+ }
309
+ /**
310
+ * Produce a populated SourceContext for the authoring agent from a cheap,
311
+ * contents-free scan. Returns null when the Source has no readable files so
312
+ * callers can fall back to the existing null (no fabricated inventory). A
313
+ * genuine read failure on the Source root returns an explicit read-error
314
+ * SourceContext instead, so an unreadable Source never silently degrades into
315
+ * a generic plan.
316
+ */
317
+ export function scanSourceContext(sourceFolderPath) {
318
+ let result;
319
+ try {
320
+ result = scan(sourceFolderPath);
321
+ }
322
+ catch (error) {
323
+ // scan() handles its own root-open failure, so a throw here is unexpected.
324
+ // Classify it the same way: a non-ENOENT errno is a read failure that must
325
+ // be surfaced, not flattened into the empty/null result; ENOENT (or a
326
+ // non-errno) stays the quiet empty case.
327
+ const errno = error;
328
+ if (errno?.code && errno.code !== "ENOENT") {
329
+ return readErrorContext(sourceFolderPath, errno);
330
+ }
331
+ return null;
332
+ }
333
+ // A read failure on the Source root with nothing enumerated is distinct from
334
+ // a genuinely empty Source: surface it loudly so the draft never goes generic
335
+ // on a false "empty" reading.
336
+ if (result.totalFiles === 0 && result.rootReadError) {
337
+ return readErrorContext(sourceFolderPath, result.rootReadError);
338
+ }
339
+ if (result.totalFiles === 0)
340
+ return null;
341
+ const items = [];
342
+ const sortedFolders = [...result.folders.entries()]
343
+ .filter(([, folder]) => folder.fileCount > 0)
344
+ .sort((a, b) => b[1].fileCount - a[1].fileCount)
345
+ .slice(0, MAX_FOLDER_ITEMS);
346
+ for (const [folderName, folder] of sortedFolders) {
347
+ const sample = folder.sampleNames.join(", ");
348
+ const noteParts = [
349
+ `${folder.fileCount} file(s)`,
350
+ kindSummary(folder.kinds),
351
+ ].filter((part) => part.length > 0);
352
+ if (sample)
353
+ noteParts.push(`e.g. ${sample}`);
354
+ items.push({
355
+ name: folderName === "." ? "(root)" : folderName,
356
+ path: folderName === "." ? "." : folderName,
357
+ kind: "folder",
358
+ note: noteParts.join(" — "),
359
+ });
360
+ }
361
+ const observations = [];
362
+ observations.push(`${result.totalFiles} file(s) across ${result.folders.size} top-level group(s).`);
363
+ const totalKindSummary = kindSummary(result.totalKinds);
364
+ if (totalKindSummary) {
365
+ observations.push(`File kinds by extension: ${totalKindSummary}.`);
366
+ }
367
+ const range = dateRange(result.dateTokens);
368
+ if (range) {
369
+ observations.push(range.min === range.max
370
+ ? `Filenames reference the period ${range.min}.`
371
+ : `Filenames span ${range.min} to ${range.max} (parsed from names, not contents).`);
372
+ }
373
+ const limitations = [
374
+ "Inventory is filename- and extension-only; Interf did not open or read any Source file. Inspect the Source through your own tools before locking stages.",
375
+ ];
376
+ if (result.truncated) {
377
+ limitations.push("Scan was capped for cost; some files or deep folders were not enumerated. Treat counts as a lower bound.");
378
+ }
379
+ return {
380
+ summary: `Contents-free Source scan: ${result.totalFiles} file(s) in ` +
381
+ `${result.folders.size} top-level group(s)` +
382
+ (totalKindSummary ? ` (${totalKindSummary})` : "") +
383
+ ".",
384
+ items,
385
+ observations: observations.slice(0, MAX_OBSERVATIONS),
386
+ limitations,
387
+ };
388
+ }
@@ -1,3 +1,4 @@
1
+ import { pickString } from "./string-utils.js";
1
2
  export function extractAgentFailureStatus(event) {
2
3
  const candidates = [];
3
4
  const directMessage = pickString(event.message) ??
@@ -42,17 +43,3 @@ export function classifyTerminalVisibleStatus(text) {
42
43
  export function hasAgentStalled(lastActivityAt, now, stallTimeoutMs) {
43
44
  return typeof stallTimeoutMs === "number" && stallTimeoutMs > 0 && now - lastActivityAt >= stallTimeoutMs;
44
45
  }
45
- function pickString(value) {
46
- if (typeof value === "string")
47
- return value;
48
- if (!Array.isArray(value))
49
- return null;
50
- for (const entry of value) {
51
- if (typeof entry === "string")
52
- return entry;
53
- if (entry && typeof entry === "object" && "text" in entry && typeof entry.text === "string") {
54
- return entry.text;
55
- }
56
- }
57
- return null;
58
- }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pick the first usable string out of a value that may be a bare string or an
3
+ * array of strings / `{ text }` content blocks. Agent event payloads carry
4
+ * message/text/content/output/summary fields in either shape across executors,
5
+ * so status-determination and render paths both reach for this.
6
+ */
7
+ export declare function pickString(value: unknown): string | null;
8
+ /**
9
+ * Pull the primary path-like field out of a tool-use `input` record for display
10
+ * summaries. Tools name the same concept differently — `file_path` (Read/Write/
11
+ * Edit), `path`, `pattern` (Glob/Grep), `command` (Bash), `notebook_path`
12
+ * (notebook tools) — so every render site collapses them with the same
13
+ * precedence. Returns an empty string when no field is present, matching the
14
+ * `?? ""` default the call sites relied on.
15
+ */
16
+ export declare function extractInputPath(input: Record<string, unknown>): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pick the first usable string out of a value that may be a bare string or an
3
+ * array of strings / `{ text }` content blocks. Agent event payloads carry
4
+ * message/text/content/output/summary fields in either shape across executors,
5
+ * so status-determination and render paths both reach for this.
6
+ */
7
+ export function pickString(value) {
8
+ if (typeof value === "string")
9
+ return value;
10
+ if (!Array.isArray(value))
11
+ return null;
12
+ for (const entry of value) {
13
+ if (typeof entry === "string")
14
+ return entry;
15
+ if (entry && typeof entry === "object" && "text" in entry && typeof entry.text === "string") {
16
+ return entry.text;
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ /**
22
+ * Pull the primary path-like field out of a tool-use `input` record for display
23
+ * summaries. Tools name the same concept differently — `file_path` (Read/Write/
24
+ * Edit), `path`, `pattern` (Glob/Grep), `command` (Bash), `notebook_path`
25
+ * (notebook tools) — so every render site collapses them with the same
26
+ * precedence. Returns an empty string when no field is present, matching the
27
+ * `?? ""` default the call sites relied on.
28
+ */
29
+ export function extractInputPath(input) {
30
+ return (input.file_path ??
31
+ input.path ??
32
+ input.pattern ??
33
+ input.command ??
34
+ input.notebook_path ??
35
+ "");
36
+ }
@@ -25,6 +25,7 @@ export interface AgentAutomationReadiness {
25
25
  export interface SpawnAgentOptions {
26
26
  eventLogPath?: string | null;
27
27
  statusLogPath?: string | null;
28
+ reasoningLogPath?: string | null;
28
29
  executionProfile?: AgentExecutionProfile;
29
30
  completionCheck?: (() => boolean) | null;
30
31
  onStatus?: (line: string) => void;
@@ -36,6 +36,8 @@ export const codexProvider = {
36
36
  "-s",
37
37
  SANDBOX_MODE,
38
38
  "--skip-git-repo-check",
39
+ "-c",
40
+ 'service_tier="fast"',
39
41
  ];
40
42
  if (profile.model) {
41
43
  args.push("--model", profile.model);
@@ -40,9 +40,10 @@ function agentRecordToAgent(record) {
40
40
  */
41
41
  export function buildRoleExecutorBundle(input) {
42
42
  const executors = new Map();
43
+ const executionProfile = input.executionProfile ?? input.defaultExecutor.executionProfile;
43
44
  for (const record of input.agents) {
44
45
  const agent = agentRecordToAgent(record);
45
- executors.set(record.name, createLocalAgentExecutor(agent, input.executionProfile));
46
+ executors.set(record.name, createLocalAgentExecutor(agent, executionProfile));
46
47
  }
47
48
  // Make sure the active / default executor is reachable by name
48
49
  // even when the active agent isn't formally in the registry yet
@@ -9,7 +9,7 @@
9
9
  * tests can swap `~/.interf/` via the `INTERF_USER_HOME` env var and
10
10
  * exercise auth flows without touching the real home dir.
11
11
  */
12
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
14
  import { z } from "zod";
15
15
  import { interfHomeRoot } from "../../contracts/lib/project-paths.js";
@@ -66,7 +66,14 @@ export function readSession() {
66
66
  /** Persist a session. Creates the auth dir if it does not exist. */
67
67
  export function writeSession(account) {
68
68
  const path = sessionPath();
69
- mkdirSync(dirname(path), { recursive: true });
69
+ const dir = dirname(path);
70
+ // `mode` on mkdir/writeFile only applies when the entry is *created*; node
71
+ // ignores it when the dir/file already exists. A session file (or auth dir)
72
+ // left over from an earlier run with looser perms (e.g. 0o644) would never be
73
+ // re-tightened. chmod after the write so the PII session file is always 0o600
74
+ // inside a 0o700 dir, even when it pre-existed.
75
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
76
+ chmodSync(dir, 0o700);
70
77
  const file = {
71
78
  kind: "interf-session",
72
79
  version: 1,
@@ -77,7 +84,8 @@ export function writeSession(account) {
77
84
  refreshed_at: account.refreshed_at,
78
85
  };
79
86
  const validated = SessionFileSchema.parse(file);
80
- writeFileSync(path, `${JSON.stringify(validated, null, 2)}\n`);
87
+ writeFileSync(path, `${JSON.stringify(validated, null, 2)}\n`, { mode: 0o600 });
88
+ chmodSync(path, 0o600);
81
89
  }
82
90
  /** Clear the session (logout). Idempotent. */
83
91
  export function clearSession() {
@@ -14,7 +14,10 @@ export declare function draftBenchmarkQuestions(options: {
14
14
  executor: AgentExecutor;
15
15
  targetCount?: number;
16
16
  onStatus?: (line: string) => void;
17
+ preservedShellRoot?: string;
17
18
  }): Promise<{
18
19
  checks: BenchmarkCheck[] | null;
19
20
  error?: string;
21
+ shellPath?: string;
22
+ reasoningPath?: string;
20
23
  }>;
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
+ import { freezePreservedShell } from "./agents/lib/shell-fs.js";
5
6
  const DraftBenchmarkQuestionSchema = z.object({
6
7
  question: z.string().min(1),
7
8
  answer: z.string().min(1),
@@ -54,11 +55,20 @@ export function buildBenchmarkQuestionDraftPrompt(options) {
54
55
  ].join("\n");
55
56
  }
56
57
  export async function draftBenchmarkQuestions(options) {
57
- const tempDir = mkdtempSync(join(tmpdir(), "interf-benchmark-question-draft-"));
58
- mkdirSync(join(tempDir, "runtime"), { recursive: true });
59
- const outputPath = join(tempDir, "questions.json");
60
- const promptPath = join(tempDir, "prompt.txt");
61
- writeFileSync(join(tempDir, "runtime", "source-locator.json"), `${JSON.stringify({
58
+ const preserve = Boolean(options.preservedShellRoot);
59
+ const shellRoot = options.preservedShellRoot
60
+ ? (mkdirSync(options.preservedShellRoot, { recursive: true }), options.preservedShellRoot)
61
+ : mkdtempSync(join(tmpdir(), "interf-benchmark-question-draft-"));
62
+ mkdirSync(join(shellRoot, "runtime"), { recursive: true });
63
+ const outputPath = join(shellRoot, "questions.json");
64
+ const promptPath = join(shellRoot, "prompt.txt");
65
+ // Same canonical convention as the stage path: reasoning is teed into the
66
+ // shell's runtime/ dir so it is preserved when the shell is frozen.
67
+ const reasoningLogPath = join(shellRoot, "runtime", "agent-reasoning.jsonl");
68
+ const statusLogPath = join(shellRoot, "runtime", "draft.status.log");
69
+ const eventLogPath = join(shellRoot, "runtime", "draft.events.ndjson");
70
+ const verdictPath = join(shellRoot, "runtime", "verdict.json");
71
+ writeFileSync(join(shellRoot, "runtime", "source-locator.json"), `${JSON.stringify({
62
72
  kind: "interf-source-locator",
63
73
  version: 1,
64
74
  generated_at: new Date().toISOString(),
@@ -77,48 +87,67 @@ export async function draftBenchmarkQuestions(options) {
77
87
  targetCount: options.targetCount ?? 4,
78
88
  });
79
89
  writeFileSync(promptPath, `${prompt}\n`);
90
+ // Record the parse/validate verdict as a structured field in the shell (the
91
+ // graph-less analogue of a stage's terminal verdict), so a reader inspecting
92
+ // the preserved shell sees WHY the draft passed or failed, not just its output.
93
+ const writeVerdict = (verdict) => {
94
+ if (!preserve)
95
+ return;
96
+ try {
97
+ writeFileSync(verdictPath, `${JSON.stringify({ kind: "interf-benchmark-question-draft-verdict", version: 1, ...verdict }, null, 2)}\n`);
98
+ }
99
+ catch {
100
+ // Best-effort: a missing verdict file must never fail the draft.
101
+ }
102
+ };
103
+ const finishPaths = preserve ? { shellPath: shellRoot, reasoningPath: reasoningLogPath } : {};
80
104
  try {
81
- const code = await options.executor.execute(tempDir, prompt, {
105
+ const code = await options.executor.execute(shellRoot, prompt, {
106
+ eventLogPath: preserve ? eventLogPath : null,
107
+ statusLogPath: preserve ? statusLogPath : null,
108
+ reasoningLogPath: preserve ? reasoningLogPath : null,
82
109
  completionCheck: () => existsSync(outputPath),
83
110
  onStatus: options.onStatus,
84
111
  });
85
112
  if (!existsSync(outputPath)) {
86
- return {
87
- checks: null,
88
- error: code === 0
89
- ? "The local agent finished without writing draft benchmark questions."
90
- : "The local agent did not produce draft benchmark questions.",
91
- };
113
+ const error = code === 0
114
+ ? "The local agent finished without writing draft benchmark questions."
115
+ : "The local agent did not produce draft benchmark questions.";
116
+ writeVerdict({ ok: false, summary: error });
117
+ return { checks: null, error, ...finishPaths };
92
118
  }
93
119
  let parsed;
94
120
  try {
95
121
  parsed = JSON.parse(readFileSync(outputPath, "utf8"));
96
122
  }
97
123
  catch (error) {
98
- return {
99
- checks: null,
100
- error: `Draft benchmark questions were not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
101
- };
124
+ const summary = `Draft benchmark questions were not valid JSON: ${error instanceof Error ? error.message : String(error)}`;
125
+ writeVerdict({ ok: false, summary });
126
+ return { checks: null, error: summary, ...finishPaths };
102
127
  }
103
128
  const validated = DraftBenchmarkQuestionsSchema.safeParse(parsed);
104
129
  if (!validated.success) {
105
130
  const detail = validated.error.issues[0]?.message ?? "invalid draft benchmark questions";
106
- return {
107
- checks: null,
108
- error: `Draft benchmark questions did not match the required shape: ${detail}`,
109
- };
131
+ const summary = `Draft benchmark questions did not match the required shape: ${detail}`;
132
+ writeVerdict({ ok: false, summary });
133
+ return { checks: null, error: summary, ...finishPaths };
110
134
  }
111
- return {
112
- checks: validated.data,
113
- };
135
+ writeVerdict({ ok: true, summary: `Drafted ${validated.data.length} benchmark question(s).` });
136
+ return { checks: validated.data, ...finishPaths };
114
137
  }
115
138
  catch (error) {
116
- return {
117
- checks: null,
118
- error: error instanceof Error ? error.message : String(error),
119
- };
139
+ const summary = error instanceof Error ? error.message : String(error);
140
+ writeVerdict({ ok: false, summary });
141
+ return { checks: null, error: summary, ...finishPaths };
120
142
  }
121
143
  finally {
122
- rmSync(tempDir, { recursive: true, force: true });
144
+ // Preserve a durable shell (freeze materializes symlinks + writes the
145
+ // preserved-shell manifest, path unchanged); only remove an ephemeral one.
146
+ if (preserve) {
147
+ freezePreservedShell(shellRoot, "benchmark-question-draft");
148
+ }
149
+ else {
150
+ rmSync(shellRoot, { recursive: true, force: true });
151
+ }
123
152
  }
124
153
  }