@tea-agent/loop-agent 0.16.26 → 0.17.1

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 (104) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +14 -3
  3. package/dist/cli/command-definitions.js +43 -0
  4. package/dist/cli/program.js +26 -0
  5. package/dist/commands/dag-approve.js +4 -0
  6. package/dist/commands/dag-resume.js +1 -0
  7. package/dist/commands/dag-validate.js +6 -0
  8. package/dist/commands/operator.js +44 -0
  9. package/dist/commands/task-contract.js +271 -0
  10. package/dist/executors/dag-pi-executor.js +55 -7
  11. package/dist/executors/pi-executor.js +206 -13
  12. package/dist/executors/pi-sdk-executor.js +21 -6
  13. package/dist/executors/shell-executor.js +55 -8
  14. package/dist/executors/shell-presets.js +16 -3
  15. package/dist/shared/operator/capabilities.js +255 -0
  16. package/dist/shared/operator/envelope.js +59 -0
  17. package/dist/shared/operator/index.js +4 -0
  18. package/dist/shared/operator/registry.js +38 -0
  19. package/dist/shared/operator/types.js +5 -0
  20. package/dist/task/contract/adopt.js +166 -0
  21. package/dist/task/contract/apply.js +326 -0
  22. package/dist/task/contract/canonicalize.js +60 -0
  23. package/dist/task/contract/constants.js +29 -0
  24. package/dist/task/contract/diff.js +177 -0
  25. package/dist/task/contract/hash.js +42 -0
  26. package/dist/task/contract/import-revision.js +96 -0
  27. package/dist/task/contract/index.js +17 -0
  28. package/dist/task/contract/journal.js +155 -0
  29. package/dist/task/contract/lock.js +153 -0
  30. package/dist/task/contract/observe.js +296 -0
  31. package/dist/task/contract/paths.js +19 -0
  32. package/dist/task/contract/project.js +170 -0
  33. package/dist/task/contract/recover.js +312 -0
  34. package/dist/task/contract/request-ledger.js +37 -0
  35. package/dist/task/contract/schema.js +151 -0
  36. package/dist/task/contract/transaction.js +160 -0
  37. package/dist/task/contract/types.js +1 -0
  38. package/dist/task/contract/validate-draft.js +106 -0
  39. package/dist/task/index.js +3 -0
  40. package/dist/task/operator/capabilities.js +6 -0
  41. package/dist/task/operator/envelope.js +2 -0
  42. package/dist/task/operator/index.js +5 -0
  43. package/dist/task/operator/registry.js +2 -0
  44. package/dist/task/operator/types.js +1 -0
  45. package/dist/task/runtime.js +5 -1
  46. package/dist/task/source-references.js +7 -0
  47. package/dist/worker/cli.js +150 -32
  48. package/dist/worker/console/app-data.js +185 -0
  49. package/dist/worker/console/dag-confirmation.js +313 -0
  50. package/dist/worker/console/doctor.js +169 -0
  51. package/dist/worker/console/draft-store.js +80 -0
  52. package/dist/worker/console/index.js +15 -0
  53. package/dist/worker/console/interview/assessment.js +67 -0
  54. package/dist/worker/console/interview/session.js +100 -0
  55. package/dist/worker/console/interview/tools.js +109 -0
  56. package/dist/worker/console/loopback.js +16 -0
  57. package/dist/worker/console/observe-health-match.js +174 -0
  58. package/dist/worker/console/observe-link.js +33 -0
  59. package/dist/worker/console/operation-runner.js +166 -0
  60. package/dist/worker/console/operation-sse.js +158 -0
  61. package/dist/worker/console/operation-store.js +147 -0
  62. package/dist/worker/console/operator-actions.js +769 -0
  63. package/dist/worker/console/pi-readiness.js +266 -0
  64. package/dist/worker/console/recovery-cta.js +133 -0
  65. package/dist/worker/console/repo-fingerprint.js +29 -0
  66. package/dist/worker/console/resource-loader.js +95 -0
  67. package/dist/worker/console/routes.js +368 -0
  68. package/dist/worker/console/security.js +126 -0
  69. package/dist/worker/console/server.js +149 -0
  70. package/dist/worker/console/sibling-controller.js +28 -0
  71. package/dist/worker/console/static/assets/index-BEIdBogJ.js +9 -0
  72. package/dist/worker/console/static/assets/index-Rt0TqimP.css +1 -0
  73. package/dist/worker/console/static/index.html +13 -0
  74. package/dist/worker/console/vite.config.js +27 -0
  75. package/dist/worker/materialize/harness-task-materializer.js +34 -0
  76. package/dist/worker/observe/health.js +57 -0
  77. package/dist/worker/observe/routes.js +16 -9
  78. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  79. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  80. package/dist/workflows/dag/frontend-repair.js +1 -10
  81. package/dist/workflows/dag/init-hybrid.js +269 -116
  82. package/dist/workflows/dag/node-execution.js +25 -1
  83. package/dist/workflows/dag/retry-policy.js +16 -1
  84. package/dist/workflows/dag/runner.js +9 -0
  85. package/dist/workflows/dag/task-contract-binding.js +138 -0
  86. package/dist/workflows/dag/types.js +74 -10
  87. package/dist/workflows/dag/validate.js +25 -7
  88. package/docs/README.md +2 -0
  89. package/docs/architecture/evolution.md +2 -0
  90. package/docs/architecture/system-overview.md +6 -0
  91. package/docs/architecture/worker-and-feature.md +7 -0
  92. package/docs/templates/agent-dag.schema.json +49 -2
  93. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  94. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  95. package/docs/templates/backend-test-dag.json +26 -154
  96. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  97. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  98. package/package.json +8 -2
  99. package/skills/agent-worker/SKILL.md +1 -0
  100. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  101. package/skills/frontend-design-review/SKILL.md +25 -16
  102. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  103. package/skills/loop-agent/references/command-reference.md +48 -1
  104. package/skills/loop-agent/references/hybrid-dag.md +4 -4
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Pi readiness gate for Official Console Happy Path.
3
+ * setup-required | ready | degraded — no pure form fallback.
4
+ */
5
+ import path from "node:path";
6
+ const SETUP_BLOCKED = Object.freeze([
7
+ "newTask",
8
+ "importPrd",
9
+ "contractApply",
10
+ "contractAdopt",
11
+ "dagRunTask",
12
+ "runDag",
13
+ "prepareDagConfirmation",
14
+ "interviewStart",
15
+ "interviewTurn",
16
+ ]);
17
+ const DEGRADED_BLOCKED = Object.freeze([
18
+ "contractApply",
19
+ "contractAdopt",
20
+ "dagRunTask",
21
+ "runDag",
22
+ // new Interview may resume; brand-new apply/run still blocked
23
+ ]);
24
+ /** Default Interview / DAG model matrix (provider, modelId). */
25
+ const INTERVIEW_MODEL_CANDIDATES = Object.freeze([
26
+ ["openai", "gpt-5.5"],
27
+ ["openai", "gpt-5.3-codex-spark"],
28
+ ["zai", "glm-5.2"],
29
+ ["zhipu", "glm-5.2"],
30
+ ]);
31
+ export function classifyPiReadiness(probe) {
32
+ if (!probe.sdkAvailable ||
33
+ !probe.authReady ||
34
+ !probe.modelsAvailable ||
35
+ !probe.interviewSessionOk) {
36
+ // Temporary provider issues after prior readiness → degraded when only
37
+ // session fails but sdk/auth/models were previously ok is hard to know;
38
+ // rule: missing core setup → setup-required; only session flaky → degraded.
39
+ if (!probe.sdkAvailable || !probe.authReady || !probe.modelsAvailable) {
40
+ return "setup-required";
41
+ }
42
+ return "degraded";
43
+ }
44
+ return "ready";
45
+ }
46
+ export function buildPiReadinessReport(probe, now = new Date().toISOString()) {
47
+ const state = classifyPiReadiness(probe);
48
+ const blockedActions = state === "setup-required"
49
+ ? SETUP_BLOCKED
50
+ : state === "degraded"
51
+ ? DEGRADED_BLOCKED
52
+ : Object.freeze([]);
53
+ return {
54
+ state,
55
+ probe,
56
+ generatedAt: now,
57
+ blockedActions,
58
+ };
59
+ }
60
+ export function isActionBlockedByReadiness(state, action) {
61
+ if (state === "ready")
62
+ return false;
63
+ if (state === "setup-required") {
64
+ return SETUP_BLOCKED.includes(action);
65
+ }
66
+ return DEGRADED_BLOCKED.includes(action);
67
+ }
68
+ function countAvailableModels(snapshot) {
69
+ return Array.isArray(snapshot.available) ? snapshot.available.length : 0;
70
+ }
71
+ function hasProviderConfig(snapshot) {
72
+ const authKeys = snapshot.auth && typeof snapshot.auth === "object"
73
+ ? Object.keys(snapshot.auth).length
74
+ : 0;
75
+ const configured = snapshot.configuredProviders &&
76
+ typeof snapshot.configuredProviders === "object"
77
+ ? Object.keys(snapshot.configuredProviders).length
78
+ : 0;
79
+ const stored = snapshot.storedProviders && typeof snapshot.storedProviders === "object"
80
+ ? Object.keys(snapshot.storedProviders).length
81
+ : 0;
82
+ return authKeys > 0 || configured > 0 || stored > 0;
83
+ }
84
+ /**
85
+ * Discover live Pi SDK / auth / model readiness via ModelRuntime.
86
+ * Kept inside worker/console (dynamic import) so Worker does not depend on
87
+ * executors; contract matches pi-sdk-executor 0.80.10.
88
+ */
89
+ export async function discoverPiReadinessProbe() {
90
+ let sdk;
91
+ try {
92
+ sdk = (await import("@earendil-works/pi-coding-agent"));
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ return {
97
+ sdkAvailable: false,
98
+ authReady: false,
99
+ modelsAvailable: false,
100
+ interviewSessionOk: false,
101
+ details: {
102
+ sdkMessage: `Pi SDK import failed: ${message}`,
103
+ authMessage: "skipped (sdk unavailable)",
104
+ modelsMessage: "skipped (sdk unavailable)",
105
+ sessionMessage: "skipped (sdk unavailable)",
106
+ },
107
+ };
108
+ }
109
+ const ModelRuntime = sdk.ModelRuntime;
110
+ if (typeof sdk.createAgentSession !== "function" ||
111
+ typeof sdk.getAgentDir !== "function" ||
112
+ typeof ModelRuntime?.create !== "function") {
113
+ return {
114
+ sdkAvailable: false,
115
+ authReady: false,
116
+ modelsAvailable: false,
117
+ interviewSessionOk: false,
118
+ details: {
119
+ sdkMessage: "Pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.80.10)",
120
+ authMessage: "skipped (sdk incompatible)",
121
+ modelsMessage: "skipped (sdk incompatible)",
122
+ sessionMessage: "skipped (sdk incompatible)",
123
+ },
124
+ };
125
+ }
126
+ let agentDir;
127
+ try {
128
+ agentDir = sdk.getAgentDir();
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ return {
133
+ sdkAvailable: false,
134
+ authReady: false,
135
+ modelsAvailable: false,
136
+ interviewSessionOk: false,
137
+ details: {
138
+ sdkMessage: `getAgentDir failed: ${message}`,
139
+ authMessage: "skipped",
140
+ modelsMessage: "skipped",
141
+ sessionMessage: "skipped",
142
+ },
143
+ };
144
+ }
145
+ let runtime;
146
+ try {
147
+ runtime = await ModelRuntime.create({
148
+ authPath: path.join(agentDir, "auth.json"),
149
+ modelsPath: path.join(agentDir, "models.json"),
150
+ });
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ return {
155
+ sdkAvailable: true,
156
+ authReady: false,
157
+ modelsAvailable: false,
158
+ interviewSessionOk: false,
159
+ details: {
160
+ sdkMessage: "Pi SDK 0.80.10 contract available",
161
+ authMessage: `ModelRuntime.create failed: ${message}`,
162
+ modelsMessage: "skipped (runtime init failed)",
163
+ sessionMessage: "skipped (runtime init failed)",
164
+ },
165
+ };
166
+ }
167
+ const snapshot = runtime.snapshot ?? {};
168
+ const availableCount = countAvailableModels(snapshot);
169
+ const providerConfigured = hasProviderConfig(snapshot);
170
+ const resolvedInterviewModels = [];
171
+ if (typeof runtime.getModel === "function") {
172
+ for (const [provider, modelId] of INTERVIEW_MODEL_CANDIDATES) {
173
+ try {
174
+ if (runtime.getModel(provider, modelId)) {
175
+ resolvedInterviewModels.push(`${provider}/${modelId}`);
176
+ }
177
+ }
178
+ catch {
179
+ // ignore per-model resolution errors
180
+ }
181
+ }
182
+ }
183
+ const authReady = availableCount > 0 || providerConfigured;
184
+ const modelsAvailable = availableCount > 0 || resolvedInterviewModels.length > 0;
185
+ let interviewSessionOk = false;
186
+ let sessionMessage;
187
+ if (!authReady || !modelsAvailable) {
188
+ sessionMessage =
189
+ "Interview session preflight skipped (auth/models not ready)";
190
+ }
191
+ else if (typeof sdk.SessionManager?.inMemory !== "function") {
192
+ sessionMessage =
193
+ "SessionManager.inMemory unavailable; Interview session preflight failed";
194
+ }
195
+ else {
196
+ try {
197
+ sdk.SessionManager.inMemory(process.cwd());
198
+ interviewSessionOk = true;
199
+ sessionMessage = `Interview session preflight ok (${availableCount} available model(s); matrix=${resolvedInterviewModels.join(",") || "none"})`;
200
+ }
201
+ catch (error) {
202
+ const message = error instanceof Error ? error.message : String(error);
203
+ sessionMessage = `Interview session preflight failed: ${message}`;
204
+ }
205
+ }
206
+ return {
207
+ sdkAvailable: true,
208
+ authReady,
209
+ modelsAvailable,
210
+ interviewSessionOk,
211
+ details: {
212
+ sdkMessage: `Pi SDK 0.80.10 contract available (agentDir=${agentDir})`,
213
+ authMessage: authReady
214
+ ? `credentials/providers ready (available=${availableCount}, providerConfig=${providerConfigured})`
215
+ : "no available models and no stored/configured provider credentials (set Pi auth or provider API keys)",
216
+ modelsMessage: modelsAvailable
217
+ ? `models ready (available=${availableCount}, interviewMatrix=${resolvedInterviewModels.join(",") || "none"})`
218
+ : "no available models for Interview",
219
+ sessionMessage,
220
+ },
221
+ };
222
+ }
223
+ /**
224
+ * Default probe for process boot. Discovers live Pi ModelRuntime when no
225
+ * override is injected. Tests inject override for determinism.
226
+ */
227
+ export async function probePiReadiness(options) {
228
+ if (options?.override) {
229
+ return buildPiReadinessReport(options.override);
230
+ }
231
+ try {
232
+ const probe = await discoverPiReadinessProbe();
233
+ if (options?.siblingControllerOk && !probe.details) {
234
+ probe.details = {};
235
+ }
236
+ if (options?.siblingControllerOk && probe.details) {
237
+ probe.details.sdkMessage = `${probe.details.sdkMessage ?? "sdk probed"}; sibling controller identity ok`;
238
+ }
239
+ return buildPiReadinessReport(probe);
240
+ }
241
+ catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ return buildPiReadinessReport({
244
+ sdkAvailable: false,
245
+ authReady: false,
246
+ modelsAvailable: false,
247
+ interviewSessionOk: false,
248
+ details: {
249
+ sdkMessage: `Pi readiness discovery error: ${message}`,
250
+ authMessage: "no credential probe completed",
251
+ modelsMessage: "no model list available",
252
+ sessionMessage: "Interview session preflight not run",
253
+ },
254
+ });
255
+ }
256
+ }
257
+ export function readinessGateFailure(report, action) {
258
+ if (!isActionBlockedByReadiness(report.state, action))
259
+ return undefined;
260
+ return {
261
+ code: "PI_SETUP_REQUIRED",
262
+ message: `action "${action}" is blocked while pi readiness is ${report.state} (no pure form fallback)`,
263
+ state: report.state,
264
+ action,
265
+ };
266
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Recovery UI CTA matrix (design §5.5.3).
3
+ * Pure table-driven policy — no Cancel, no primary "edit code" CTA.
4
+ */
5
+ const CTA = {
6
+ report: {
7
+ id: "report",
8
+ label: "查看报告",
9
+ action: "status",
10
+ commandHint: "loop-agent dag report --run-id <run-id>",
11
+ },
12
+ doctor: {
13
+ id: "doctor",
14
+ label: "Doctor 诊断",
15
+ action: "doctor",
16
+ commandHint: "agent-worker console doctor --repo .",
17
+ },
18
+ decision: {
19
+ id: "decision",
20
+ label: "人工决策",
21
+ commandHint: "loop-agent dag doctor --run-id <run-id> --markdown",
22
+ },
23
+ resume: {
24
+ id: "resume",
25
+ label: "恢复运行",
26
+ commandHint: "loop-agent run-dag --resume --run-id <run-id>",
27
+ },
28
+ reconcile: {
29
+ id: "reconcile",
30
+ label: "对账 / reconcile",
31
+ requiresReason: true,
32
+ commandHint: "loop-agent dag reconcile-run --run-id <run-id> --reason <structured-reason>",
33
+ },
34
+ regenerate: {
35
+ id: "regenerate",
36
+ label: "重新生成契约/计划",
37
+ commandHint: "loop-agent dag run-task <task-id> --profile auto",
38
+ },
39
+ observeLink: {
40
+ id: "observeLink",
41
+ label: "打开 Observe 深链",
42
+ action: "observeLink",
43
+ commandHint: "agent-worker observe serve --repo . --port 8787",
44
+ },
45
+ };
46
+ const MATRIX = {
47
+ "validation-failed": ["report", "doctor", "regenerate"],
48
+ "contract-drift": ["report", "doctor", "decision", "reconcile", "regenerate"],
49
+ "runtime-mismatch": ["report", "doctor", "reconcile", "regenerate"],
50
+ "pause-on-human": ["decision", "resume", "report", "doctor"],
51
+ "verification-failed": ["report", "doctor", "resume", "regenerate"],
52
+ "orphan-unknown": ["report", "doctor", "reconcile"],
53
+ "terminal-failed": ["report", "doctor", "resume", "regenerate"],
54
+ "terminal-succeeded": ["report", "observeLink"],
55
+ "needs-reconcile": ["report", "doctor", "reconcile", "regenerate"],
56
+ };
57
+ const FORBIDDEN_LABELS = [
58
+ "直接改代码",
59
+ "edit code",
60
+ "Cancel",
61
+ "cancel operation",
62
+ ];
63
+ /**
64
+ * Return allowed recovery CTAs for a fact/failure class.
65
+ * Never includes Cancel or primary "edit code".
66
+ */
67
+ export function allowedRecoveryCtas(factClass) {
68
+ const ids = MATRIX[factClass] ?? MATRIX["orphan-unknown"];
69
+ return ids.map((id) => CTA[id]);
70
+ }
71
+ /**
72
+ * Negative product invariants for recovery matrix (test + docs).
73
+ */
74
+ export function recoveryMatrixInvariants() {
75
+ const allCtaIds = Object.keys(CTA);
76
+ const factClasses = Object.keys(MATRIX);
77
+ return {
78
+ forbiddenLabels: FORBIDDEN_LABELS,
79
+ allCtaIds,
80
+ factClasses,
81
+ };
82
+ }
83
+ /**
84
+ * Best-effort classification from operation/DAG surface strings for MVP UI.
85
+ * Prefer structured fields when available; unknown → orphan-unknown.
86
+ */
87
+ export function classifyRecoveryFact(input) {
88
+ if (input.needsReconcile)
89
+ return "needs-reconcile";
90
+ const code = (input.errorCode ?? "").toLowerCase();
91
+ const state = (input.operationState ?? "").toLowerCase();
92
+ const dag = (input.dagStatus ?? "").toLowerCase();
93
+ if (code.includes("validation") ||
94
+ code.includes("invalid") ||
95
+ state.includes("validation")) {
96
+ return "validation-failed";
97
+ }
98
+ if (code.includes("contract") ||
99
+ code.includes("drift") ||
100
+ code.includes("binding")) {
101
+ return "contract-drift";
102
+ }
103
+ if (code.includes("runtime") ||
104
+ code.includes("mismatch") ||
105
+ code.includes("fingerprint")) {
106
+ return "runtime-mismatch";
107
+ }
108
+ if (code.includes("human") ||
109
+ state.includes("paused") ||
110
+ dag.includes("paused") ||
111
+ state.includes("awaiting")) {
112
+ return "pause-on-human";
113
+ }
114
+ if (code.includes("verif") || state.includes("verif")) {
115
+ return "verification-failed";
116
+ }
117
+ if (state === "succeeded" ||
118
+ state === "completed" ||
119
+ dag === "succeeded" ||
120
+ dag === "completed") {
121
+ return "terminal-succeeded";
122
+ }
123
+ if (state === "failed" ||
124
+ state.includes("fail") ||
125
+ dag === "failed" ||
126
+ code.includes("fail")) {
127
+ return "terminal-failed";
128
+ }
129
+ if (code.includes("reconcil") || state.includes("reconcil")) {
130
+ return "needs-reconcile";
131
+ }
132
+ return "orphan-unknown";
133
+ }
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import path from "node:path";
4
+ /**
5
+ * Normalize a worktree root for repoFingerprintV1:
6
+ * realpath → Windows drive letter uppercased → `/` separators → strip trailing `/`
7
+ * (except a bare root). No extra case-folding on macOS/Windows.
8
+ */
9
+ export function normalizeWorktreeRealpath(repoRoot) {
10
+ let resolved = realpathSync(path.resolve(repoRoot));
11
+ if (/^[a-zA-Z]:/.test(resolved)) {
12
+ resolved = resolved[0].toUpperCase() + resolved.slice(1);
13
+ }
14
+ resolved = resolved.replace(/\\/g, "/");
15
+ if (resolved.length > 1 && resolved.endsWith("/")) {
16
+ resolved = resolved.replace(/\/+$/, "");
17
+ }
18
+ return resolved;
19
+ }
20
+ /**
21
+ * Design §5.1: sha256("loop-console-repo-v1\0" + normalizedWorktreeRealpath)
22
+ * Returns lowercase hex digest (no sha256: prefix).
23
+ */
24
+ export function repoFingerprintV1(repoRoot) {
25
+ const normalized = normalizeWorktreeRealpath(repoRoot);
26
+ return createHash("sha256")
27
+ .update(`loop-console-repo-v1\0${normalized}`, "utf8")
28
+ .digest("hex");
29
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Official Console ResourceLoader (Phase 2).
3
+ * Closed surface: no edit/write/bash/full-tools business tools.
4
+ * Runtime env MUST NOT restore denied tools.
5
+ * Requirement Interview uses a stricter allowlist (see interview/tools.ts).
6
+ */
7
+ import { createInterviewResourceLoader, INTERVIEW_ALLOWED_TOOLS, INTERVIEW_DENIED_TOOLS, authorizeInterviewTool, } from "./interview/tools.js";
8
+ export const OFFICIAL_DENIED_TOOL_IDS = Object.freeze([
9
+ "edit",
10
+ "write",
11
+ "bash",
12
+ "full-tools",
13
+ "full_tools",
14
+ "coding-chat",
15
+ "coding_chat",
16
+ "shell",
17
+ "apply_patch",
18
+ "apply-patch",
19
+ // Interview / operator mutation tools must never appear as free coding tools
20
+ "contractApply",
21
+ "contract_apply",
22
+ "runDag",
23
+ "run_dag",
24
+ "reconcile",
25
+ ]);
26
+ /** Official Console surface allows Interview-scoped tools only when Interview session is active. */
27
+ export const OFFICIAL_ALLOWED_TOOL_IDS = Object.freeze([
28
+ ...INTERVIEW_ALLOWED_TOOLS,
29
+ ]);
30
+ const DENIED = new Set(OFFICIAL_DENIED_TOOL_IDS.map((id) => id.toLowerCase()));
31
+ const ALLOWED = new Set(OFFICIAL_ALLOWED_TOOL_IDS.map((id) => id.toLowerCase()));
32
+ /** Official allowed tools: Interview closed set (no mutation operator tools). */
33
+ export function listOfficialTools() {
34
+ return OFFICIAL_ALLOWED_TOOL_IDS;
35
+ }
36
+ export function isOfficialToolDenied(toolId) {
37
+ const id = toolId.trim().toLowerCase();
38
+ if (DENIED.has(id))
39
+ return true;
40
+ const compact = id.replace(/[-_]/g, "");
41
+ for (const d of DENIED) {
42
+ if (d.replace(/[-_]/g, "") === compact)
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Attempt to activate a tool on the Official Console surface.
49
+ * Write-class, full-tools, and Interview-forbidden mutation ids are always rejected.
50
+ */
51
+ export function tryActivateTool(toolId) {
52
+ const id = toolId.trim();
53
+ if (!id) {
54
+ return {
55
+ ok: false,
56
+ code: "tool-denied",
57
+ toolId: id,
58
+ message: "empty tool id is not allowed on Official Console surface",
59
+ };
60
+ }
61
+ if (isOfficialToolDenied(id)) {
62
+ return {
63
+ ok: false,
64
+ code: "tool-denied",
65
+ toolId: id,
66
+ message: `tool "${id}" is denied on Official Loop Operator Console (no edit/write/bash/full-tools/contractApply/runDag/reconcile)`,
67
+ };
68
+ }
69
+ if (!ALLOWED.has(id.toLowerCase())) {
70
+ return {
71
+ ok: false,
72
+ code: "tool-denied",
73
+ toolId: id,
74
+ message: `tool "${id}" is not registered on Official Console ResourceLoader`,
75
+ };
76
+ }
77
+ return { ok: true, toolId: id };
78
+ }
79
+ export function assertOfficialToolAllowed(toolId) {
80
+ const result = tryActivateTool(toolId);
81
+ if (!result.ok) {
82
+ throw new Error(result.message);
83
+ }
84
+ }
85
+ export function createClosedResourceLoader() {
86
+ return {
87
+ mode: "official-closed",
88
+ deniedToolIds: OFFICIAL_DENIED_TOOL_IDS,
89
+ allowedToolIds: OFFICIAL_ALLOWED_TOOL_IDS,
90
+ listTools: listOfficialTools,
91
+ tryActivateTool,
92
+ allowsEnvToolRestore: false,
93
+ };
94
+ }
95
+ export { createInterviewResourceLoader, INTERVIEW_ALLOWED_TOOLS, INTERVIEW_DENIED_TOOLS, authorizeInterviewTool, };