@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.
- package/README.md +122 -226
- package/dist/cli/commands/agents.js +1 -32
- package/dist/cli/commands/benchmark.d.ts +2 -3
- package/dist/cli/commands/benchmark.js +1 -31
- package/dist/cli/commands/build-plan.js +26 -50
- package/dist/cli/commands/build.d.ts +2 -3
- package/dist/cli/commands/build.js +1 -31
- package/dist/cli/commands/graphs.js +177 -32
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +223 -126
- package/dist/cli/commands/project.js +10 -36
- package/dist/cli/commands/reset.d.ts +2 -3
- package/dist/cli/commands/reset.js +1 -22
- package/dist/cli/commands/runs.js +86 -33
- package/dist/cli/commands/status.js +3 -24
- package/dist/cli/commands/traces.js +1 -29
- package/dist/cli/commands/wizard.js +17 -29
- package/dist/cli/lib/http-client.d.ts +39 -0
- package/dist/cli/lib/http-client.js +73 -0
- package/dist/packages/build-plans/authoring/brief.d.ts +25 -4
- package/dist/packages/build-plans/authoring/build-plan-authoring.d.ts +42 -1
- package/dist/packages/build-plans/authoring/build-plan-authoring.js +470 -63
- package/dist/packages/build-plans/authoring/build-plan-edit-session.d.ts +9 -0
- package/dist/packages/build-plans/authoring/build-plan-edit-session.js +27 -10
- package/dist/packages/build-plans/authoring/build-plan-improvement.js +62 -8
- package/dist/packages/build-plans/authoring/lib/build-plan-edit-utils.d.ts +1 -0
- package/dist/packages/build-plans/package/build-plan-definitions.d.ts +0 -1
- package/dist/packages/build-plans/package/build-plan-definitions.js +5 -3
- package/dist/packages/build-plans/package/build-plan-stage-runner.d.ts +1 -0
- package/dist/packages/build-plans/package/build-plan-stage-runner.js +2 -1
- package/dist/packages/build-plans/package/builtin-build-plan.d.ts +2 -2
- package/dist/packages/build-plans/package/builtin-build-plan.js +3 -3
- package/dist/packages/build-plans/package/context-interface.d.ts +3 -0
- package/dist/packages/build-plans/package/context-interface.js +5 -5
- package/dist/packages/build-plans/package/interf-build-plan-package.js +22 -22
- package/dist/packages/build-plans/package/local-build-plans.d.ts +10 -5
- package/dist/packages/build-plans/package/local-build-plans.js +57 -32
- package/dist/packages/contracts/index.d.ts +4 -3
- package/dist/packages/contracts/index.js +2 -1
- package/dist/packages/contracts/lib/context-graph-layer.d.ts +161 -0
- package/dist/packages/contracts/lib/context-graph-layer.js +216 -0
- package/dist/packages/contracts/lib/project-paths.d.ts +7 -0
- package/dist/packages/contracts/lib/project-paths.js +9 -0
- package/dist/packages/contracts/lib/project-schema.d.ts +264 -1
- package/dist/packages/contracts/lib/project-schema.js +38 -13
- package/dist/packages/contracts/lib/schema.d.ts +556 -23
- package/dist/packages/contracts/lib/schema.js +279 -18
- package/dist/packages/contracts/utils/filesystem.d.ts +1 -0
- package/dist/packages/contracts/utils/filesystem.js +29 -1
- package/dist/packages/projects/lib/schema.d.ts +6 -8
- package/dist/packages/projects/lib/schema.js +3 -1
- package/dist/packages/projects/source-config.d.ts +0 -5
- package/dist/packages/projects/source-config.js +9 -22
- package/dist/packages/runtime/actions/fields.d.ts +4 -0
- package/dist/packages/runtime/actions/form-builders.js +79 -31
- package/dist/packages/runtime/actions/form-validators.js +9 -3
- package/dist/packages/runtime/actions/helpers.js +3 -3
- package/dist/packages/runtime/actions/registry.d.ts +1 -1
- package/dist/packages/runtime/actions/registry.js +1 -1
- package/dist/packages/runtime/actions/requests.d.ts +1 -1
- package/dist/packages/runtime/actions/requests.js +12 -6
- package/dist/packages/runtime/actions/schemas.d.ts +7 -0
- package/dist/packages/runtime/actions/schemas.js +1 -0
- package/dist/packages/runtime/agent-handoff.js +8 -7
- package/dist/packages/runtime/agents/lib/execution-profile.d.ts +14 -0
- package/dist/packages/runtime/agents/lib/execution-profile.js +23 -0
- package/dist/packages/runtime/agents/lib/execution.js +14 -8
- package/dist/packages/runtime/agents/lib/executors.d.ts +1 -0
- package/dist/packages/runtime/agents/lib/executors.js +11 -2
- package/dist/packages/runtime/agents/lib/logs.d.ts +10 -0
- package/dist/packages/runtime/agents/lib/logs.js +32 -8
- package/dist/packages/runtime/agents/lib/preflight.js +4 -1
- package/dist/packages/runtime/agents/lib/render.d.ts +18 -0
- package/dist/packages/runtime/agents/lib/render.js +44 -18
- package/dist/packages/runtime/agents/lib/shell-templates.js +105 -63
- package/dist/packages/runtime/agents/lib/shells.d.ts +29 -0
- package/dist/packages/runtime/agents/lib/shells.js +158 -32
- package/dist/packages/runtime/agents/lib/source-context-scan.d.ts +10 -0
- package/dist/packages/runtime/agents/lib/source-context-scan.js +388 -0
- package/dist/packages/runtime/agents/lib/status.js +1 -14
- package/dist/packages/runtime/agents/lib/string-utils.d.ts +16 -0
- package/dist/packages/runtime/agents/lib/string-utils.js +36 -0
- package/dist/packages/runtime/agents/lib/types.d.ts +1 -0
- package/dist/packages/runtime/agents/providers/codex.js +2 -0
- package/dist/packages/runtime/agents/role-executors.js +2 -1
- package/dist/packages/runtime/auth/session-store.js +11 -3
- package/dist/packages/runtime/benchmark-question-draft.d.ts +3 -0
- package/dist/packages/runtime/benchmark-question-draft.js +57 -28
- package/dist/packages/runtime/build/artifact-status.d.ts +1 -1
- package/dist/packages/runtime/build/artifact-status.js +1 -1
- package/dist/packages/runtime/build/build-evidence.d.ts +2 -1
- package/dist/packages/runtime/build/build-evidence.js +11 -5
- package/dist/packages/runtime/build/build-pipeline.js +89 -5
- package/dist/packages/runtime/build/build-stage-plan.js +3 -1
- package/dist/packages/runtime/build/build-stage-runner.js +169 -32
- package/dist/packages/runtime/build/build-target.d.ts +3 -0
- package/dist/packages/runtime/build/build-target.js +25 -1
- package/dist/packages/runtime/build/check-evaluator.d.ts +1 -1
- package/dist/packages/runtime/build/check-evaluator.js +655 -4
- package/dist/packages/runtime/build/context-graph-paths.d.ts +13 -0
- package/dist/packages/runtime/build/context-graph-paths.js +27 -0
- package/dist/packages/runtime/build/index.d.ts +2 -2
- package/dist/packages/runtime/build/index.js +2 -2
- package/dist/packages/runtime/build/inspect-map.d.ts +10 -0
- package/dist/packages/runtime/build/inspect-map.js +270 -0
- package/dist/packages/runtime/build/lib/schema.d.ts +246 -53
- package/dist/packages/runtime/build/lib/schema.js +173 -15
- package/dist/packages/runtime/build/native-entrypoint.d.ts +2 -0
- package/dist/packages/runtime/build/native-entrypoint.js +286 -0
- package/dist/packages/runtime/build/runtime-contracts.js +9 -3
- package/dist/packages/runtime/build/runtime-log-paths.d.ts +3 -0
- package/dist/packages/runtime/build/runtime-log-paths.js +16 -0
- package/dist/packages/runtime/build/runtime-prompt.js +6 -4
- package/dist/packages/runtime/build/runtime-runs.js +63 -10
- package/dist/packages/runtime/build/runtime-types.d.ts +4 -1
- package/dist/packages/runtime/build/runtime.d.ts +3 -1
- package/dist/packages/runtime/build/runtime.js +3 -1
- package/dist/packages/runtime/build/source-files.js +11 -2
- package/dist/packages/runtime/build/source-inventory.d.ts +1 -0
- package/dist/packages/runtime/build/source-inventory.js +246 -7
- package/dist/packages/runtime/build/source-manifest.d.ts +11 -0
- package/dist/packages/runtime/build/source-manifest.js +30 -2
- package/dist/packages/runtime/build/stage-evidence.js +80 -11
- package/dist/packages/runtime/build/stage-manifest.d.ts +45 -0
- package/dist/packages/runtime/build/stage-manifest.js +1125 -0
- package/dist/packages/runtime/build/stage-reuse.js +12 -0
- package/dist/packages/runtime/build/stage-session.d.ts +81 -0
- package/dist/packages/runtime/build/stage-session.js +308 -0
- package/dist/packages/runtime/build/state-io.js +10 -11
- package/dist/packages/runtime/build/state-view.js +1 -1
- package/dist/packages/runtime/build/state.d.ts +1 -1
- package/dist/packages/runtime/build/state.js +1 -1
- package/dist/packages/runtime/build/summary-coverage-index.d.ts +21 -0
- package/dist/packages/runtime/build/summary-coverage-index.js +189 -0
- package/dist/packages/runtime/build/traces.js +3 -3
- package/dist/packages/runtime/build/validate-context-graph.d.ts +1 -1
- package/dist/packages/runtime/build/validate-context-graph.js +5 -5
- package/dist/packages/runtime/build/validate.d.ts +1 -1
- package/dist/packages/runtime/build/validate.js +1 -1
- package/dist/packages/runtime/client.d.ts +3 -3
- package/dist/packages/runtime/client.js +8 -13
- package/dist/packages/runtime/context-checks.js +13 -0
- package/dist/packages/runtime/context-graph-scaffold.js +2 -1
- package/dist/packages/runtime/context-graph-semantic-graph.d.ts +9 -0
- package/dist/packages/runtime/context-graph-semantic-graph.js +416 -0
- package/dist/packages/runtime/execution/lib/schema.d.ts +34 -31
- package/dist/packages/runtime/index.d.ts +2 -2
- package/dist/packages/runtime/index.js +1 -1
- package/dist/packages/runtime/native-run-handlers.d.ts +38 -0
- package/dist/packages/runtime/native-run-handlers.js +52 -33
- package/dist/packages/runtime/plan-artifact-contract.js +1 -1
- package/dist/packages/runtime/project-source-state.d.ts +4 -4
- package/dist/packages/runtime/project-source-state.js +5 -2
- package/dist/packages/runtime/project-store.d.ts +5 -0
- package/dist/packages/runtime/project-store.js +30 -3
- package/dist/packages/runtime/requested-artifacts.js +1 -1
- package/dist/packages/runtime/run-observability.js +9 -4
- package/dist/packages/runtime/runtime-action-proposals.js +3 -3
- package/dist/packages/runtime/runtime-build-plans.js +47 -3
- package/dist/packages/runtime/runtime-build-runs.js +9 -16
- package/dist/packages/runtime/runtime-caches.d.ts +26 -0
- package/dist/packages/runtime/runtime-caches.js +47 -0
- package/dist/packages/runtime/runtime-jobs.js +6 -6
- package/dist/packages/runtime/runtime-project-mutations.js +1 -0
- package/dist/packages/runtime/runtime-project-reads.d.ts +4 -1
- package/dist/packages/runtime/runtime-project-reads.js +229 -36
- package/dist/packages/runtime/runtime-proposal-helpers.js +6 -6
- package/dist/packages/runtime/runtime-resource-builders.d.ts +4 -2
- package/dist/packages/runtime/runtime-resource-builders.js +16 -14
- package/dist/packages/runtime/runtime-status.d.ts +14 -0
- package/dist/packages/runtime/runtime-status.js +15 -0
- package/dist/packages/runtime/runtime-verify-runs.js +6 -5
- package/dist/packages/runtime/runtime.d.ts +439 -22
- package/dist/packages/runtime/runtime.js +16 -2
- package/dist/packages/runtime/schemas/actions.d.ts +24 -0
- package/dist/packages/runtime/schemas/agents.d.ts +28 -0
- package/dist/packages/runtime/schemas/agents.js +33 -0
- package/dist/packages/runtime/schemas/build-plans.d.ts +181 -8
- package/dist/packages/runtime/schemas/build-plans.js +36 -2
- package/dist/packages/runtime/schemas/context-graphs.d.ts +1522 -0
- package/dist/packages/runtime/schemas/context-graphs.js +110 -0
- package/dist/packages/runtime/schemas/files.d.ts +7 -347
- package/dist/packages/runtime/schemas/files.js +1 -24
- package/dist/packages/runtime/schemas/index.d.ts +1 -0
- package/dist/packages/runtime/schemas/index.js +1 -0
- package/dist/packages/runtime/schemas/jobs.js +4 -0
- package/dist/packages/runtime/schemas/projects.d.ts +48 -21
- package/dist/packages/runtime/schemas/projects.js +34 -10
- package/dist/packages/runtime/schemas/runs.d.ts +1009 -240
- package/dist/packages/runtime/schemas/runs.js +17 -0
- package/dist/packages/runtime/service/openapi.js +1 -0
- package/dist/packages/runtime/service/operations.d.ts +1666 -145
- package/dist/packages/runtime/service/operations.js +147 -17
- package/dist/packages/runtime/service/routes.d.ts +11 -3
- package/dist/packages/runtime/service/routes.js +11 -3
- package/dist/packages/runtime/service/server-app-boot.js +2 -2
- package/dist/packages/runtime/service/server-helpers.d.ts +11 -0
- package/dist/packages/runtime/service/server-helpers.js +19 -0
- package/dist/packages/runtime/service/server-routes-action-proposals.js +4 -2
- package/dist/packages/runtime/service/server-routes-agents.js +19 -85
- package/dist/packages/runtime/service/server-routes-build-plans.js +14 -11
- package/dist/packages/runtime/service/server-routes-project-context.js +102 -7
- package/dist/packages/runtime/service/server-routes-project-jobs.js +19 -12
- package/dist/packages/runtime/service/server-routes-project-runs.js +5 -2
- package/dist/packages/runtime/service/server-routes-projects.js +6 -2
- package/dist/packages/runtime/service/server-routes-runs.js +11 -4
- package/dist/packages/runtime/verify/lib/schema.js +12 -0
- package/dist/packages/runtime/verify/test-file-guard.d.ts +2 -0
- package/dist/packages/runtime/verify/test-file-guard.js +29 -0
- package/dist/packages/runtime/verify/verify-execution.d.ts +7 -0
- package/dist/packages/runtime/verify/verify-execution.js +109 -35
- package/dist/packages/runtime/verify/verify-paths.d.ts +1 -0
- package/dist/packages/runtime/verify/verify-paths.js +4 -0
- package/dist/packages/runtime/verify/verify-specs.js +49 -39
- package/dist/packages/runtime/wire-schemas.d.ts +1 -1
- package/dist/packages/runtime/wire-schemas.js +1 -1
- package/package.json +2 -8
- package/public-repo/CONTRIBUTING.md +10 -3
- package/public-repo/README.md +122 -226
- package/public-repo/build-plans/interf-default/README.md +15 -12
- package/public-repo/build-plans/interf-default/build/stages/entrypoint/SKILL.md +74 -0
- package/public-repo/build-plans/interf-default/build/stages/knowledge/SKILL.md +95 -0
- package/public-repo/build-plans/interf-default/build/stages/summarize/SKILL.md +38 -5
- package/public-repo/build-plans/interf-default/build-plan.json +27 -23
- package/public-repo/build-plans/interf-default/build-plan.schema.json +24 -20
- package/public-repo/build-plans/interf-default/use/query/SKILL.md +8 -7
- package/public-repo/openapi/local-service.openapi.json +11637 -4213
- package/public-repo/skills/interf/SKILL.md +174 -134
- package/dist/packages/runtime/build/runtime-paths.d.ts +0 -8
- package/dist/packages/runtime/build/runtime-paths.js +0 -26
- package/dist/packages/runtime/build/state-paths.d.ts +0 -7
- package/dist/packages/runtime/build/state-paths.js +0 -22
- package/public-repo/build-plans/interf-default/build/stages/shape/SKILL.md +0 -34
- 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;
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
}
|