@felixdotgo/aidlc-kit 0.1.1 → 0.3.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/DEVELOPMENT.md +14 -13
- package/README.md +37 -43
- package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
- package/dist/assets/.agents/aidlc/orchestrator.md +2 -2
- package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
- package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
- package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
- package/dist/assets/.agents/aidlc/schemas/state.schema.json +2 -0
- package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
- package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +261 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
- package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
- package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +116 -28
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
- package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +149 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
- package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +68 -2
- package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
- package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +168 -73
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
- package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
- package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
- package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
- package/dist/assets/.agents/aidlc/templates/design.md +9 -0
- package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
- package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
- package/dist/assets/ui/bulma.min.css +3 -0
- package/dist/assets/ui/changes.js +83 -0
- package/dist/assets/ui/common.js +664 -0
- package/dist/assets/ui/dashboard.css +636 -0
- package/dist/assets/ui/dashboard.js +77 -0
- package/dist/assets/ui/detail.js +159 -0
- package/dist/assets/ui/live-log.js +332 -0
- package/dist/services/aidlc-coordination/Dockerfile +11 -3
- package/dist/services/aidlc-coordination/compose.yaml +12 -0
- package/dist/services/aidlc-coordination/package-lock.json +3 -1
- package/dist/services/aidlc-coordination/package.json +9 -4
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
- package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
- package/dist/services/aidlc-coordination/src/server.mjs +9 -3
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
- package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
- package/dist/src/adapters.d.ts +0 -3
- package/dist/src/adapters.js +3 -5
- package/dist/src/cli/argv.d.ts +12 -0
- package/dist/src/cli/argv.js +68 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.js +159 -0
- package/dist/src/cli/loop-launcher.d.ts +47 -0
- package/dist/src/cli/loop-launcher.js +322 -0
- package/dist/src/cli/options.d.ts +25 -0
- package/dist/src/cli/options.js +32 -0
- package/dist/src/cli/prompt.d.ts +9 -0
- package/dist/src/cli/prompt.js +66 -0
- package/dist/src/cli.js +28 -191
- package/dist/src/gate.js +3 -2
- package/dist/src/installer.d.ts +14 -7
- package/dist/src/installer.js +42 -143
- package/dist/src/legacy.js +1 -0
- package/dist/src/managed-content.d.ts +7 -0
- package/dist/src/managed-content.js +15 -4
- package/dist/src/managed-planning.d.ts +65 -0
- package/dist/src/managed-planning.js +219 -0
- package/dist/src/mcp-setup.js +3 -1
- package/dist/src/model/config-contracts.d.ts +65 -0
- package/dist/src/model/config-contracts.js +1 -0
- package/dist/src/model/install-contracts.d.ts +29 -0
- package/dist/src/model/install-contracts.js +1 -0
- package/dist/src/model/lifecycle-enums.d.ts +13 -0
- package/dist/src/model/lifecycle-enums.js +1 -0
- package/dist/src/model/loop.d.ts +20 -0
- package/dist/src/model/loop.js +1 -0
- package/dist/src/model/manifest.d.ts +16 -0
- package/dist/src/model/manifest.js +1 -0
- package/dist/src/model/records.d.ts +142 -0
- package/dist/src/model/records.js +1 -0
- package/dist/src/model.d.ts +11 -264
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +2 -0
- package/dist/src/profiles.js +53 -9
- package/dist/src/state/archive-record.d.ts +9 -0
- package/dist/src/state/archive-record.js +26 -0
- package/dist/src/state/commands.d.ts +18 -0
- package/dist/src/state/commands.js +27 -0
- package/dist/src/state/domain.d.ts +9 -0
- package/dist/src/state/domain.js +9 -0
- package/dist/src/state/guards.d.ts +7 -0
- package/dist/src/state/guards.js +17 -0
- package/dist/src/state/io.d.ts +10 -0
- package/dist/src/state/io.js +35 -0
- package/dist/src/state/items.d.ts +3 -4
- package/dist/src/state/items.js +58 -57
- package/dist/src/state/lessons.js +14 -7
- package/dist/src/state/lifecycle.js +2 -2
- package/dist/src/state/lock.js +7 -6
- package/dist/src/state/memory.js +24 -12
- package/dist/src/state/{archive.js → migrate.js} +7 -7
- package/dist/src/state/partition.d.ts +15 -0
- package/dist/src/state/partition.js +90 -0
- package/dist/src/state/paths.d.ts +11 -0
- package/dist/src/state/paths.js +12 -0
- package/dist/src/state/render.js +9 -6
- package/dist/src/state/store.js +9 -15
- package/dist/src/state/transitions.js +9 -7
- package/dist/src/state/validate.js +55 -7
- package/dist/src/state.d.ts +16 -11
- package/dist/src/state.js +16 -11
- package/dist/src/task-ui.d.ts +33 -0
- package/dist/src/task-ui.js +33 -0
- package/dist/src/ui/assets.d.ts +14 -0
- package/dist/src/ui/assets.js +45 -0
- package/dist/src/ui/changes.d.ts +83 -0
- package/dist/src/ui/changes.js +498 -0
- package/dist/src/ui/containers.d.ts +71 -0
- package/dist/src/ui/containers.js +214 -0
- package/dist/src/ui/daemon.d.ts +75 -0
- package/dist/src/ui/daemon.js +232 -0
- package/dist/src/ui/data.d.ts +42 -0
- package/dist/src/ui/data.js +89 -0
- package/dist/src/ui/docs.d.ts +13 -0
- package/dist/src/ui/docs.js +39 -0
- package/dist/src/ui/files.d.ts +3 -0
- package/dist/src/ui/files.js +20 -0
- package/dist/src/ui/http.d.ts +14 -0
- package/dist/src/ui/http.js +80 -0
- package/dist/src/ui/live.d.ts +5 -0
- package/dist/src/ui/live.js +43 -0
- package/dist/src/ui/markdown.d.ts +2 -0
- package/dist/src/ui/markdown.js +127 -0
- package/dist/src/ui/options.d.ts +48 -0
- package/dist/src/ui/options.js +81 -0
- package/dist/src/ui/security.d.ts +34 -0
- package/dist/src/ui/security.js +38 -0
- package/dist/src/ui/server.d.ts +39 -0
- package/dist/src/ui/server.js +347 -0
- package/dist/src/ui/sse.d.ts +14 -0
- package/dist/src/ui/sse.js +281 -0
- package/dist/src/ui/views/board.d.ts +7 -0
- package/dist/src/ui/views/board.js +131 -0
- package/dist/src/ui/views/changes.d.ts +19 -0
- package/dist/src/ui/views/changes.js +154 -0
- package/dist/src/ui/views/components.d.ts +56 -0
- package/dist/src/ui/views/components.js +113 -0
- package/dist/src/ui/views/docs.d.ts +15 -0
- package/dist/src/ui/views/docs.js +64 -0
- package/dist/src/ui/views/layout.d.ts +44 -0
- package/dist/src/ui/views/layout.js +233 -0
- package/dist/src/ui/views/task.d.ts +25 -0
- package/dist/src/ui/views/task.js +320 -0
- package/dist/src/upgrade-board.js +10 -10
- package/dist/src/upgrade.d.ts +0 -1
- package/dist/src/upgrade.js +24 -85
- package/dist/src/workflow.d.ts +4 -0
- package/dist/src/workflow.js +36 -4
- package/docs/README.md +41 -22
- package/docs/architecture.md +101 -37
- package/docs/command-reference.md +289 -52
- package/docs/configuration.md +70 -30
- package/docs/coordination-service.md +91 -30
- package/docs/design-decisions.md +87 -0
- package/docs/development.md +124 -49
- package/docs/loop-local-run.md +228 -0
- package/docs/loop-runbook.md +214 -0
- package/docs/operating-workflow.md +55 -42
- package/docs/testing-and-release.md +72 -49
- package/docs/troubleshooting.md +221 -0
- package/package.json +4 -1
- package/dist/src/ledger.d.ts +0 -22
- package/dist/src/ledger.js +0 -119
- package/dist/src/state/shared.d.ts +0 -29
- package/dist/src/state/shared.js +0 -71
- package/dist/src/tracker.d.ts +0 -36
- package/dist/src/tracker.js +0 -44
- /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
|
@@ -9,21 +9,69 @@ import { delimiter, join } from "node:path";
|
|
|
9
9
|
const MAX_LINE_BYTES = 1024 * 1024;
|
|
10
10
|
const MAX_STDERR_BYTES = 4096;
|
|
11
11
|
const MAX_RESULT_CHARS = 8192;
|
|
12
|
+
const MAX_STREAM_CHARS = 4096;
|
|
12
13
|
const MAX_DIAGNOSTICS = 16;
|
|
14
|
+
/** Upper bound for the human-readable `summary` carried by live-log events. */
|
|
15
|
+
export const MAX_SUMMARY_CHARS = 240;
|
|
13
16
|
|
|
14
17
|
/** One runtime enumeration for every loop provider. Host gates import this
|
|
15
18
|
* list; container paths reject providers that need file-based credentials. */
|
|
16
19
|
export const LOOP_PROVIDERS = Object.freeze(["codex", "claude", "opencode"]);
|
|
17
20
|
export const LOOP_PROVIDER_LIST = `${LOOP_PROVIDERS.slice(0, -1).join(", ")}, or ${LOOP_PROVIDERS[LOOP_PROVIDERS.length - 1]}`;
|
|
18
21
|
export const isLoopProvider = (value) => typeof value === "string" && LOOP_PROVIDERS.includes(value);
|
|
19
|
-
const OPENCODE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*(?:#[A-Za-z0-9][A-Za-z0-9._-]*)?$/;
|
|
22
|
+
export const OPENCODE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*(?:#[A-Za-z0-9][A-Za-z0-9._-]*)?$/;
|
|
20
23
|
|
|
21
24
|
const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
25
|
+
// Best-effort secret redaction for diagnostics and live-log summaries (commands
|
|
26
|
+
// now reach the event log). Over-redaction is acceptable; leaks are not.
|
|
27
|
+
const SECRET_NAME = String.raw`[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIALS?|AUTH)[A-Z0-9_.-]*`;
|
|
22
28
|
const redact = (value) => String(value ?? "")
|
|
23
|
-
|
|
29
|
+
// HTTP auth headers first, so the scheme word is not mistaken for the secret.
|
|
30
|
+
.replace(/\b(Authorization\s*:\s*(?:Basic|Bearer|Token|Digest)?\s*)[^\s'"]+/gi, "$1[REDACTED]")
|
|
31
|
+
.replace(/\b(x-api-key|api-key|x-auth-token|private-token|x-access-token)(\s*:\s*)[^\s'"]+/gi, "$1$2[REDACTED]")
|
|
32
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*/gi, "$1 [REDACTED]")
|
|
33
|
+
// NAME=value / NAME: value for secret-looking names (env assignments, config, JSON-ish).
|
|
34
|
+
.replace(new RegExp(`\\b(${SECRET_NAME})(\\s*[=:]\\s*)("[^"]*"|'[^']*'|[^\\s'"]+)`, "gi"), "$1$2[REDACTED]")
|
|
35
|
+
// --password value / --token=value style flags.
|
|
36
|
+
.replace(/((?<![\w-])--?(?:password|passwd|pass|pwd|token|secret|api-?key|access-?key|private-?key|auth|credentials?)(?:[= ]+))("[^"]*"|'[^']*'|[^\s'"]+)/gi, "$1[REDACTED]")
|
|
37
|
+
// Credentials embedded in URLs (scheme://user:pass@host).
|
|
38
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi, "$1[REDACTED]@")
|
|
39
|
+
// curl -u user:pass / --user user:pass.
|
|
40
|
+
.replace(/(\s(?:-u|--user)(?:\s+|=))[^\s:]+:[^\s]+/g, "$1[REDACTED]")
|
|
41
|
+
// mysql -pSECRET (password glued to -p).
|
|
42
|
+
.replace(/(\bmysql(?:dump|admin)?\b[^\n]*?\s-p)(\S+)/gi, "$1[REDACTED]")
|
|
43
|
+
// Well-known token formats.
|
|
24
44
|
.replace(/\b(?:sk|ghp|github_pat)_[A-Za-z0-9_-]+\b/g, "[REDACTED]")
|
|
45
|
+
.replace(/\b(?:sk|gh[pousr]|github_pat|npm|pypi|glpat|hf|xox[abposr])[_-][A-Za-z0-9_-]{8,}/g, "[REDACTED]")
|
|
46
|
+
.replace(/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED]")
|
|
47
|
+
.replace(/\bAIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]")
|
|
48
|
+
.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g, "[REDACTED]")
|
|
49
|
+
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[REDACTED]")
|
|
25
50
|
.replace(/\s+/g, " ").trim();
|
|
26
51
|
const diagnostic = (value) => redact(value).slice(0, 512);
|
|
52
|
+
/** Human-readable, secret-redacted, single-line event summary (at most
|
|
53
|
+
* MAX_SUMMARY_CHARS). Returns undefined for empty input so callers can omit the
|
|
54
|
+
* field. Shared by every live-log event producer (lead drivers and workers). */
|
|
55
|
+
const SUMMARY_SCAN_CHARS = 4096;
|
|
56
|
+
export function summarize(value) {
|
|
57
|
+
if (value === undefined || value === null) return undefined;
|
|
58
|
+
// Only the first MAX_SUMMARY_CHARS survive, so redacting a bounded prefix leaks
|
|
59
|
+
// nothing and keeps the regexes linear-time on huge inputs (e.g. base64 blobs).
|
|
60
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
61
|
+
const text = redact(raw.slice(0, SUMMARY_SCAN_CHARS));
|
|
62
|
+
if (!text) return undefined;
|
|
63
|
+
return text.length > MAX_SUMMARY_CHARS ? `${text.slice(0, MAX_SUMMARY_CHARS - 1)}…` : text;
|
|
64
|
+
}
|
|
65
|
+
const SUMMARY_INPUT_FIELDS = ["command", "file_path", "filePath", "path", "notebook_path"];
|
|
66
|
+
/** Summary of a tool invocation from its structured input: the shell command or
|
|
67
|
+
* the touched file when present, otherwise the tool name and its input keys
|
|
68
|
+
* (never the values of unknown inputs). */
|
|
69
|
+
export function toolSummary(name, input) {
|
|
70
|
+
const fields = record(input);
|
|
71
|
+
for (const key of SUMMARY_INPUT_FIELDS) if (typeof fields?.[key] === "string" && fields[key].trim()) return summarize(fields[key]);
|
|
72
|
+
const keys = fields ? Object.keys(fields).slice(0, 8) : [];
|
|
73
|
+
return summarize(keys.length ? `${name ?? "tool"} ${JSON.stringify(keys)}` : name);
|
|
74
|
+
}
|
|
27
75
|
const isId = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\s\x00-\x1f]/.test(value);
|
|
28
76
|
const emit = async (callback, event) => { if (callback) await callback(event); };
|
|
29
77
|
|
|
@@ -51,8 +99,16 @@ registerAgentAdapter("codex", {
|
|
|
51
99
|
if (event.type === "turn.completed") context.complete();
|
|
52
100
|
if (event.type === "turn.failed" || event.type === "error") throw new Error(`codex reported ${event.type}: ${diagnostic(JSON.stringify(event.error ?? event.message ?? "unknown"))}`);
|
|
53
101
|
const item = record(event.item);
|
|
54
|
-
if (event.type === "item.completed" && item?.type === "agent_message" && typeof item.text === "string") context.result(item.text);
|
|
55
|
-
if (event.type === "item.completed" && item?.type === "command_execution")
|
|
102
|
+
if (event.type === "item.completed" && item?.type === "agent_message" && typeof item.text === "string") { context.result(item.text); await context.stream(item.text); }
|
|
103
|
+
if (event.type === "item.completed" && item?.type === "command_execution") {
|
|
104
|
+
const command = typeof item.command === "string" ? item.command : undefined;
|
|
105
|
+
await context.tool("shell", summarize(command));
|
|
106
|
+
await context.commandEvent(item.exit_code, item.status, command);
|
|
107
|
+
}
|
|
108
|
+
if (event.type === "item.completed" && item?.type === "file_change" && Array.isArray(item.changes)) {
|
|
109
|
+
const paths = item.changes.map((change) => record(change)?.path).filter((path) => typeof path === "string" && path);
|
|
110
|
+
await context.tool("file_change", summarize(paths.join(", ")));
|
|
111
|
+
}
|
|
56
112
|
}
|
|
57
113
|
});
|
|
58
114
|
registerAgentAdapter("claude", {
|
|
@@ -69,10 +125,15 @@ registerAgentAdapter("claude", {
|
|
|
69
125
|
const message = record(event.message);
|
|
70
126
|
if (Array.isArray(message?.content)) for (const block of message.content) {
|
|
71
127
|
if (!record(block)) continue;
|
|
72
|
-
if (block.type === "tool_use" && typeof block.
|
|
128
|
+
if (block.type === "tool_use" && typeof block.name === "string" && block.name) {
|
|
129
|
+
await context.tool(block.name, toolSummary(block.name, block.input));
|
|
130
|
+
// Only shell commands map to lifecycle_command; remember the command for the result.
|
|
131
|
+
if (typeof block.id === "string" && block.name === "Bash") context.toolUses.set(block.id, typeof record(block.input)?.command === "string" ? block.input.command : "");
|
|
132
|
+
}
|
|
73
133
|
if (block.type === "tool_result" && typeof block.tool_use_id === "string" && context.toolUses.has(block.tool_use_id)) {
|
|
134
|
+
const command = context.toolUses.get(block.tool_use_id);
|
|
74
135
|
context.toolUses.delete(block.tool_use_id);
|
|
75
|
-
await context.commandEvent(block.is_error === true ? 1 : 0, block.is_error === true ? "failed" : "completed");
|
|
136
|
+
await context.commandEvent(block.is_error === true ? 1 : 0, block.is_error === true ? "failed" : "completed", command);
|
|
76
137
|
}
|
|
77
138
|
}
|
|
78
139
|
}
|
|
@@ -88,12 +149,18 @@ registerAgentAdapter("opencode", {
|
|
|
88
149
|
},
|
|
89
150
|
parse: async (event, context) => {
|
|
90
151
|
if (event.sessionID !== undefined) await context.captureId(event.sessionID);
|
|
152
|
+
if (event.type === "error") {
|
|
153
|
+
const info = record(event.error);
|
|
154
|
+
throw new Error(`opencode reported an error: ${diagnostic(info?.message ?? info?.type ?? JSON.stringify(event.error ?? "unknown"))}`);
|
|
155
|
+
}
|
|
91
156
|
const part = record(event.part);
|
|
92
|
-
if (event.type === "text" && typeof part?.text === "string") context.result(part.text);
|
|
157
|
+
if (event.type === "text" && typeof part?.text === "string") { context.result(part.text); await context.stream(part.text); }
|
|
158
|
+
const state = record(part?.state);
|
|
159
|
+
if (event.type === "tool_use" && typeof part?.tool === "string") await context.tool(part.tool, toolSummary(part.tool, state?.input));
|
|
93
160
|
if (event.type === "tool_use" && part?.tool === "shell") {
|
|
94
|
-
const state = record(part.state);
|
|
95
161
|
const metadata = record(record(state?.metadata)?.metadata);
|
|
96
|
-
|
|
162
|
+
const command = record(state?.input)?.command;
|
|
163
|
+
await context.commandEvent(metadata?.exit, state?.status === "error" ? "failed" : state?.status, typeof command === "string" ? command : undefined);
|
|
97
164
|
}
|
|
98
165
|
if (event.type === "step_finish" && part?.reason === "stop") context.complete();
|
|
99
166
|
}
|
|
@@ -105,6 +172,16 @@ registerAgentAdapter("opencode", {
|
|
|
105
172
|
* a checkpoint is durable. Transport and protocol failures reject with a bounded,
|
|
106
173
|
* redacted error message.
|
|
107
174
|
*/
|
|
175
|
+
/** Host turn environment: drops the worker credentials `loop-env.mjs` added from the env file
|
|
176
|
+
* or provider config (listed in AIDLC_LEAD_ENV_EXCLUDE); shell-exported values stay. */
|
|
177
|
+
export function leadEnv(env) {
|
|
178
|
+
const exclude = String(env.AIDLC_LEAD_ENV_EXCLUDE ?? "").split(",").map((key) => key.trim()).filter(Boolean);
|
|
179
|
+
if (!exclude.length && env.AIDLC_LEAD_ENV_EXCLUDE === undefined) return env;
|
|
180
|
+
const copy = { ...env };
|
|
181
|
+
for (const key of [...exclude, "AIDLC_LEAD_ENV_EXCLUDE"]) delete copy[key];
|
|
182
|
+
return copy;
|
|
183
|
+
}
|
|
184
|
+
|
|
108
185
|
export async function runAgentTurn({ agent, root, sessionId, prompt, timeoutMs, onSessionId, onEvent, signal, env, spawnProcess = spawn } = {}) {
|
|
109
186
|
// --- Validate inputs and resolve the adapter -------------------------------
|
|
110
187
|
const adapter = adapters.get(agent);
|
|
@@ -123,7 +200,7 @@ export async function runAgentTurn({ agent, root, sessionId, prompt, timeoutMs,
|
|
|
123
200
|
if (typeof spawnProcess !== "function") throw new Error("spawnProcess must be a function");
|
|
124
201
|
const input = adapter.input?.(prompt);
|
|
125
202
|
if (input !== undefined && typeof input !== "string") throw new Error("agent adapter input must be a string");
|
|
126
|
-
const child = spawnProcess(adapter.command, args, { cwd: root, shell: false, env: { ...(env ?? process.env), PWD: root }, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
203
|
+
const child = spawnProcess(adapter.command, args, { cwd: root, shell: false, env: { ...leadEnv(env ?? process.env), PWD: root }, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
127
204
|
if (input !== undefined) {
|
|
128
205
|
if (!child.stdin) throw new Error("agent transport lacks stdin");
|
|
129
206
|
child.stdin.on("error", () => {});
|
|
@@ -162,18 +239,27 @@ export async function runAgentTurn({ agent, root, sessionId, prompt, timeoutMs,
|
|
|
162
239
|
await emit(onSessionId, value);
|
|
163
240
|
}
|
|
164
241
|
};
|
|
165
|
-
const commandEvent = async (exitCode, status) => {
|
|
242
|
+
const commandEvent = async (exitCode, status, command) => {
|
|
166
243
|
const safeExitCode = Number.isSafeInteger(exitCode) ? exitCode : null;
|
|
167
244
|
const safeStatus = ["completed", "failed", "in_progress"].includes(status) ? status : "unknown";
|
|
168
|
-
|
|
245
|
+
// The command string is a redacted summary only; command output never leaves the adapter.
|
|
246
|
+
const summary = summarize(command);
|
|
247
|
+
await emit(onEvent, { type: "lifecycle_command", exitCode: safeExitCode, status: safeStatus, ...(summary ? { summary } : {}) });
|
|
169
248
|
if ((safeExitCode !== null && safeExitCode !== 0) || safeStatus === "failed") addDiagnostic(`commandFailed exitCode=${String(safeExitCode ?? "unknown")} status=${safeStatus}`);
|
|
170
249
|
};
|
|
250
|
+
const stream = async (text) => {
|
|
251
|
+
if (typeof text === "string" && text.trim()) await emit(onEvent, { type: "agent_text", text: text.slice(0, MAX_STREAM_CHARS) });
|
|
252
|
+
};
|
|
253
|
+
const tool = async (name, summary) => {
|
|
254
|
+
if (typeof name !== "string" || !name) return;
|
|
255
|
+
const safeSummary = typeof summary === "string" ? summarize(summary) : undefined;
|
|
256
|
+
await emit(onEvent, { type: "agent_tool", tool: name, ...(safeSummary ? { summary: safeSummary } : {}) });
|
|
257
|
+
};
|
|
171
258
|
const parseEvent = async (line) => {
|
|
172
259
|
let event;
|
|
173
260
|
try { event = JSON.parse(line); } catch { throw new Error(`${agent} emitted malformed JSONL`); }
|
|
174
261
|
if (!record(event) || typeof event.type !== "string") throw new Error(`${agent} emitted malformed event`);
|
|
175
|
-
await adapter.parse(event, { captureId, commandEvent, toolUses, complete: () => { completed = true; resultSeen = true; }, result: (value) => { resultText = value.slice(0, MAX_RESULT_CHARS); } });
|
|
176
|
-
await emit(onEvent, { type: "agent_event", eventType: event.type });
|
|
262
|
+
await adapter.parse(event, { captureId, commandEvent, toolUses, stream, tool, complete: () => { completed = true; resultSeen = true; }, result: (value) => { resultText = value.slice(0, MAX_RESULT_CHARS); } });
|
|
177
263
|
};
|
|
178
264
|
const queueLine = (line) => {
|
|
179
265
|
processing = processing.then(() => parseEvent(line)).catch((error) => fail(error.message));
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-config environment for the local loop daemon.
|
|
3
|
+
*
|
|
4
|
+
* Merges three sources, highest priority first:
|
|
5
|
+
* 1. the invoking shell (`process.env`) — never overridden;
|
|
6
|
+
* 2. the AIDLC env file (`AIDLC_ENV_FILE`, default `.agents/loop.env`), a minimal dotenv;
|
|
7
|
+
* 3. values derived from the configured providers' own config files:
|
|
8
|
+
* - `AIDLC_LOOP_EVENT_LOG` → `.agents/data/loop/ui-events.jsonl` (dashboard live log);
|
|
9
|
+
* - `AIDLC_OPENCODE_MODEL` → `model` of the OpenCode config (project > OPENCODE_CONFIG > global);
|
|
10
|
+
* - worker credentials → OpenCode `auth.json`, Codex `$CODEX_HOME/auth.json`
|
|
11
|
+
* (`OPENAI_API_KEY`), Claude Code settings `env.ANTHROPIC_API_KEY` (local > project > user).
|
|
12
|
+
*
|
|
13
|
+
* Only providers listed in `loop.agents` are inspected. Secret values are never printed,
|
|
14
|
+
* logged, written to disk, or returned in a report; the report names their source only.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
20
|
+
import { isLoopProvider, OPENCODE_MODEL } from "./loop-drivers.mjs";
|
|
21
|
+
import { workerCredentialKeys } from "./loop-worker-docker.mjs";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_ENV_FILE = ".agents/loop.env";
|
|
24
|
+
const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
26
|
+
/** The only values a report may show; every other variable (any env-file key) is "set"/"missing". */
|
|
27
|
+
const DISPLAY_KEYS = new Set(["AIDLC_OPENCODE_MODEL", "AIDLC_LOOP_EVENT_LOG", "AIDLC_WORKER_DOCKER_IMAGE", "AIDLC_LOCAL_WORKTREES_ROOT", "AIDLC_ENV_FILE"]);
|
|
28
|
+
|
|
29
|
+
export const isSecretKey = (key) => !DISPLAY_KEYS.has(key);
|
|
30
|
+
|
|
31
|
+
/** Minimal dotenv: `KEY=VALUE`, optional `export `, `#` comments, single/double quotes. No expansion. */
|
|
32
|
+
export function parseEnvFile(text, label = "env file") {
|
|
33
|
+
const values = {};
|
|
34
|
+
const lines = String(text).replace(/^/, "").split(/\r?\n/);
|
|
35
|
+
lines.forEach((raw, index) => {
|
|
36
|
+
const line = raw.trim();
|
|
37
|
+
if (!line || line.startsWith("#")) return;
|
|
38
|
+
const match = /^(?:export\s+)?([^=\s]+)\s*=\s*(.*)$/.exec(line);
|
|
39
|
+
if (!match || !KEY.test(match[1])) throw new Error(`${label}:${index + 1}: expected KEY=VALUE`);
|
|
40
|
+
const [, key, rest] = match;
|
|
41
|
+
let value = rest;
|
|
42
|
+
const quote = value[0];
|
|
43
|
+
if (quote === '"' || quote === "'") {
|
|
44
|
+
const end = value.indexOf(quote, 1);
|
|
45
|
+
if (end < 0) throw new Error(`${label}:${index + 1}: unterminated ${quote === '"' ? "double" : "single"} quote`);
|
|
46
|
+
const tail = value.slice(end + 1).trim();
|
|
47
|
+
if (tail && !tail.startsWith("#")) throw new Error(`${label}:${index + 1}: unexpected text after the closing quote`);
|
|
48
|
+
value = value.slice(1, end);
|
|
49
|
+
if (quote === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
50
|
+
} else {
|
|
51
|
+
value = value.replace(/\s+#.*$/, "").trim();
|
|
52
|
+
}
|
|
53
|
+
values[key] = value;
|
|
54
|
+
});
|
|
55
|
+
return values;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The env file to load: `AIDLC_ENV_FILE` (relative to root) or the default; `explicit` marks a user choice. */
|
|
59
|
+
export function resolveEnvFile(root, env = process.env) {
|
|
60
|
+
const chosen = typeof env.AIDLC_ENV_FILE === "string" && env.AIDLC_ENV_FILE.trim() ? env.AIDLC_ENV_FILE.trim() : "";
|
|
61
|
+
return { path: resolve(root, chosen || DEFAULT_ENV_FILE), explicit: Boolean(chosen) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const realOrSelf = (path) => { try { return realpathSync(path); } catch { return resolve(path); } };
|
|
65
|
+
const within = (base, target) => { const rel = relative(base, target); return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads a small regular file (symlinks are followed). With `confine`, a symlinked file must
|
|
69
|
+
* resolve inside the project or the home directory, both compared by their real paths so a
|
|
70
|
+
* project reached through a symlink (macOS /tmp, ~/code -> /mnt/...) still works.
|
|
71
|
+
*/
|
|
72
|
+
function readTrustedFile(path, { root, home }, { confine = false } = {}) {
|
|
73
|
+
if (!existsSync(path)) return undefined;
|
|
74
|
+
const real = realpathSync(path);
|
|
75
|
+
if (confine && lstatSync(path).isSymbolicLink() && !within(realOrSelf(root), real) && !(home && within(realOrSelf(home), real))) throw new Error(`${path} links outside the project and home directory`);
|
|
76
|
+
const stat = statSync(real);
|
|
77
|
+
if (!stat.isFile()) throw new Error(`${path} is not a regular file`);
|
|
78
|
+
if (stat.size > MAX_FILE_BYTES) throw new Error(`${path} is larger than 1 MiB`);
|
|
79
|
+
return readFileSync(real, "utf8");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** JSON with `//` and block comments and trailing commas (OpenCode `.jsonc`). Strings are kept intact. */
|
|
83
|
+
export function parseJsonc(text) {
|
|
84
|
+
let out = "";
|
|
85
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
86
|
+
const char = text[i];
|
|
87
|
+
if (char === '"') {
|
|
88
|
+
let j = i + 1;
|
|
89
|
+
while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
|
|
90
|
+
out += text.slice(i, j + 1); i = j; continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i += 1; out += "\n"; continue; }
|
|
93
|
+
if (char === "/" && text[i + 1] === "*") { const end = text.indexOf("*/", i + 2); i = end < 0 ? text.length : end + 1; continue; }
|
|
94
|
+
out += char;
|
|
95
|
+
}
|
|
96
|
+
return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const readJson = (path, context, { jsonc = false } = {}) => {
|
|
100
|
+
try {
|
|
101
|
+
const text = readTrustedFile(path, context);
|
|
102
|
+
return text === undefined ? undefined : jsonc ? parseJsonc(text) : JSON.parse(text);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Never echo a parser message: V8 quotes the source text around the error, which may be a key.
|
|
105
|
+
const reason = error instanceof SyntaxError ? "not valid JSON" : error instanceof Error ? error.message : String(error);
|
|
106
|
+
context.problems.push({ level: "warning", key: "", message: `ignored ${path}: ${reason}` });
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Unique providers of `loop.agents` (lead first), and the worker subset. */
|
|
112
|
+
export function loopProviders(config) {
|
|
113
|
+
const lead = config?.loop?.agents?.lead;
|
|
114
|
+
const workers = Object.values(config?.loop?.agents?.workers ?? {}).filter(isLoopProvider);
|
|
115
|
+
return { lead: isLoopProvider(lead) ? lead : undefined, workers: [...new Set(workers)], all: [...new Set([...(isLoopProvider(lead) ? [lead] : []), ...workers])] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const xdg = (env, name, home, fallback) => (env[name] && isAbsolute(env[name]) ? env[name] : join(home, fallback));
|
|
119
|
+
|
|
120
|
+
/** OpenCode `model`, following its documented precedence (later wins): global < OPENCODE_CONFIG < project. */
|
|
121
|
+
function opencodeModel(context) {
|
|
122
|
+
const { root, env, home } = context;
|
|
123
|
+
const configDir = join(xdg(env, "XDG_CONFIG_HOME", home, ".config"), "opencode");
|
|
124
|
+
const candidates = [
|
|
125
|
+
join(configDir, "opencode.json"), join(configDir, "opencode.jsonc"),
|
|
126
|
+
...(env.OPENCODE_CONFIG ? [resolve(root, env.OPENCODE_CONFIG)] : []),
|
|
127
|
+
join(root, "opencode.json"), join(root, "opencode.jsonc"),
|
|
128
|
+
];
|
|
129
|
+
let found;
|
|
130
|
+
for (const path of candidates) {
|
|
131
|
+
const config = readJson(path, context, { jsonc: true });
|
|
132
|
+
if (typeof config?.model === "string" && config.model.trim()) found = { value: config.model.trim(), source: `derived:${path}` };
|
|
133
|
+
}
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Worker credentials a provider's own login/config can supply, as `{ key, value, source }`. */
|
|
138
|
+
function providerCredentials(provider, env, context) {
|
|
139
|
+
const { root, home } = context;
|
|
140
|
+
if (provider === "opencode") {
|
|
141
|
+
const path = join(xdg(env, "XDG_DATA_HOME", home, join(".local", "share")), "opencode", "auth.json");
|
|
142
|
+
const auth = readJson(path, context);
|
|
143
|
+
if (!auth || typeof auth !== "object") return [];
|
|
144
|
+
const prefix = String(env.AIDLC_OPENCODE_MODEL ?? "").split("/")[0];
|
|
145
|
+
const keys = workerCredentialKeys("opencode", env);
|
|
146
|
+
// The OpenCode Zen/Go key only stands in for OPENCODE_API_KEY, never for a provider's own key.
|
|
147
|
+
const ids = keys.includes("OPENCODE_API_KEY") ? [prefix, "opencode", "opencode-go"] : [prefix];
|
|
148
|
+
const entry = ids.map((id) => auth[id]).find((value) => value?.type === "api" && typeof value.key === "string" && value.key);
|
|
149
|
+
return entry ? keys.map((key) => ({ key, value: entry.key, source: `derived:${path}` })) : [];
|
|
150
|
+
}
|
|
151
|
+
if (provider === "codex") {
|
|
152
|
+
const path = join(env.CODEX_HOME && isAbsolute(env.CODEX_HOME) ? env.CODEX_HOME : join(home, ".codex"), "auth.json");
|
|
153
|
+
const auth = readJson(path, context);
|
|
154
|
+
if (typeof auth?.OPENAI_API_KEY === "string" && auth.OPENAI_API_KEY) return [{ key: "OPENAI_API_KEY", value: auth.OPENAI_API_KEY, source: `derived:${path}` }];
|
|
155
|
+
if (auth?.tokens) context.notes.push({ provider, message: "Codex is signed in with ChatGPT; Docker workers need an API key (codex login --with-api-key, or OPENAI_API_KEY in the env file)" });
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
if (provider === "claude") {
|
|
159
|
+
const userDir = env.CLAUDE_CONFIG_DIR && isAbsolute(env.CLAUDE_CONFIG_DIR) ? env.CLAUDE_CONFIG_DIR : join(home, ".claude");
|
|
160
|
+
let found = [];
|
|
161
|
+
for (const path of [join(userDir, "settings.json"), join(root, ".claude", "settings.json"), join(root, ".claude", "settings.local.json")]) {
|
|
162
|
+
const value = readJson(path, context)?.env?.ANTHROPIC_API_KEY;
|
|
163
|
+
if (typeof value === "string" && value) found = [{ key: "ANTHROPIC_API_KEY", value, source: `derived:${path}` }];
|
|
164
|
+
}
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** True when `command` resolves on PATH (PATHEXT-aware on Windows). */
|
|
171
|
+
export function hasCommand(command, env = process.env) {
|
|
172
|
+
const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
173
|
+
return String(env.PATH ?? "").split(delimiter).filter(Boolean).some((dir) => exts.some((ext) => { try { return statSync(join(dir, command + ext)).isFile(); } catch { return false; } }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Whether git would ignore the env file; outside a work tree there is nothing to commit. */
|
|
177
|
+
const gitignored = (root, envFile) => {
|
|
178
|
+
const rel = relative(root, envFile);
|
|
179
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return true;
|
|
180
|
+
const result = spawnSync("git", ["check-ignore", "-q", "--", rel.split(sep).join("/")], { cwd: root, stdio: "ignore", timeout: 5000 });
|
|
181
|
+
return result.error ? true : result.status !== 1;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolves the daemon environment. `applied` holds only the keys to add to the
|
|
186
|
+
* process (file + derived); shell keys are left untouched. `problems` with level
|
|
187
|
+
* `error` must stop the daemon before it starts.
|
|
188
|
+
*/
|
|
189
|
+
export function loadLoopEnv({ root, config, env = process.env, home = homedir(), commandExists = hasCommand } = {}) {
|
|
190
|
+
root = resolve(root ?? ".");
|
|
191
|
+
const problems = [], notes = [];
|
|
192
|
+
const context = { root, env, home, problems, notes };
|
|
193
|
+
const providers = loopProviders(config);
|
|
194
|
+
const mode = config?.loop?.mode ?? "off";
|
|
195
|
+
const sources = {}, applied = {};
|
|
196
|
+
const merged = { ...env };
|
|
197
|
+
// Anything the shell exports, even an empty value, is never overridden.
|
|
198
|
+
const set = (key, value, source) => { if (merged[key] !== undefined) return false; merged[key] = value; applied[key] = value; sources[key] = source; return true; };
|
|
199
|
+
for (const key of Object.keys(env)) sources[key] = "shell";
|
|
200
|
+
|
|
201
|
+
const envFile = resolveEnvFile(root, env);
|
|
202
|
+
let fileKeys = [];
|
|
203
|
+
try {
|
|
204
|
+
const text = readTrustedFile(envFile.path, context, { confine: true });
|
|
205
|
+
if (text === undefined) {
|
|
206
|
+
if (envFile.explicit) problems.push({ level: "error", key: "AIDLC_ENV_FILE", message: `env file not found: ${envFile.path}` });
|
|
207
|
+
} else {
|
|
208
|
+
const values = parseEnvFile(text, relative(root, envFile.path) || envFile.path);
|
|
209
|
+
fileKeys = Object.keys(values);
|
|
210
|
+
for (const [key, value] of Object.entries(values)) if (value !== "") set(key, value, "file");
|
|
211
|
+
if (!gitignored(root, envFile.path)) problems.push({ level: "warning", key: "", message: `${relative(root, envFile.path)} is not git-ignored; add "loop.env" to .agents/.gitignore so credentials are never committed` });
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
problems.push({ level: "error", key: "AIDLC_ENV_FILE", message: error instanceof Error ? error.message : String(error) });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (mode !== "local" && mode !== "fleet") problems.push({ level: "error", key: "", message: 'loop.mode is off: set "loop": { "mode": "local", "agents": { "lead": "<codex|claude|opencode>", "workers": { "worker-a": "<provider>" } } } in .agents/config.json, or run: aidlc-kit init --loop-mode local' });
|
|
218
|
+
else if (!providers.lead || !providers.workers.length) problems.push({ level: "error", key: "", message: 'loop.agents needs a lead and at least one worker provider (codex, claude, or opencode) in .agents/config.json' });
|
|
219
|
+
|
|
220
|
+
set("AIDLC_LOOP_EVENT_LOG", join(root, ".agents", "data", "loop", "ui-events.jsonl"), "derived");
|
|
221
|
+
|
|
222
|
+
const keys = new Set(["AIDLC_LOOP_EVENT_LOG"]);
|
|
223
|
+
if (providers.all.includes("opencode")) {
|
|
224
|
+
keys.add("AIDLC_OPENCODE_MODEL");
|
|
225
|
+
if (!merged.AIDLC_OPENCODE_MODEL) { const found = opencodeModel(context); if (found) set("AIDLC_OPENCODE_MODEL", found.value, found.source); }
|
|
226
|
+
const model = merged.AIDLC_OPENCODE_MODEL;
|
|
227
|
+
if (model === "" && sources.AIDLC_OPENCODE_MODEL === "shell") problems.push({ level: "error", key: "AIDLC_OPENCODE_MODEL", message: "AIDLC_OPENCODE_MODEL is exported empty in the shell, which overrides the env file and opencode.json: unset it or set provider/model" });
|
|
228
|
+
else if (!model) problems.push({ level: "error", key: "AIDLC_OPENCODE_MODEL", message: `opencode needs a model: add AIDLC_OPENCODE_MODEL=<provider/model> to ${relative(root, envFile.path)} or set "model" in opencode.json (list models with: opencode models)` });
|
|
229
|
+
else if (!OPENCODE_MODEL.test(model)) problems.push({ level: "error", key: "AIDLC_OPENCODE_MODEL", message: `AIDLC_OPENCODE_MODEL must look like provider/model[#variant] (from ${sources.AIDLC_OPENCODE_MODEL})` });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const workerKeys = new Set();
|
|
233
|
+
for (const provider of providers.workers) {
|
|
234
|
+
const needed = workerCredentialKeys(provider, merged);
|
|
235
|
+
needed.forEach((key) => workerKeys.add(key));
|
|
236
|
+
if (needed.every((key) => merged[key])) { needed.forEach((key) => keys.add(key)); continue; }
|
|
237
|
+
for (const credential of providerCredentials(provider, merged, context)) set(credential.key, credential.value, credential.source);
|
|
238
|
+
for (const key of needed) {
|
|
239
|
+
keys.add(key);
|
|
240
|
+
if (!merged[key]) {
|
|
241
|
+
const note = notes.find((entry) => entry.provider === provider);
|
|
242
|
+
problems.push({ level: "warning", key, message: note ? note.message : `${provider} workers need ${key} for the build phase: add it to ${relative(root, envFile.path)}` });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (mode === "local" && !commandExists("docker", merged)) problems.push({ level: "warning", key: "", message: "docker was not found on PATH; local workers run in Docker during the build phase" });
|
|
248
|
+
for (const key of ["AIDLC_WORKER_DOCKER_IMAGE", "AIDLC_LOCAL_WORKTREES_ROOT", ...fileKeys]) if (merged[key]) keys.add(key);
|
|
249
|
+
|
|
250
|
+
const entries = [...keys].map((key) => {
|
|
251
|
+
const secret = isSecretKey(key);
|
|
252
|
+
const present = Boolean(merged[key]);
|
|
253
|
+
return { key, set: present, source: present ? sources[key] ?? "shell" : null, secret, ...(present && !secret ? { value: merged[key] } : {}) };
|
|
254
|
+
});
|
|
255
|
+
// Worker keys detected from another tool's config reach Docker workers only, never the
|
|
256
|
+
// host lead (a claude/codex lead would otherwise switch from its own login to API-key
|
|
257
|
+
// billing). Env-file keys are explicit, like shell exports, and reach the lead too.
|
|
258
|
+
const leadExclude = [...workerKeys].filter((key) => key in applied && String(sources[key]).startsWith("derived:"));
|
|
259
|
+
if (leadExclude.length) applied.AIDLC_LEAD_ENV_EXCLUDE = leadExclude.join(",");
|
|
260
|
+
return { root, mode, providers: providers.all, envFile: { path: envFile.path, explicit: envFile.explicit, loaded: fileKeys.length > 0 || existsSync(envFile.path) }, entries, problems, applied, ok: !problems.some((problem) => problem.level === "error") };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A report safe to print: the `applied` values are dropped. */
|
|
264
|
+
export const publicReport = ({ applied: _applied, ...report }) => report;
|
|
265
|
+
|
|
266
|
+
export function formatLoopEnvReport(report) {
|
|
267
|
+
const lines = [];
|
|
268
|
+
lines.push(`Loop environment (${report.mode} mode)`);
|
|
269
|
+
lines.push(` providers : ${report.providers.length ? report.providers.join(", ") : "none configured"}`);
|
|
270
|
+
lines.push(` env file : ${report.envFile.path}${report.envFile.loaded ? "" : " (not found)"}`);
|
|
271
|
+
const width = Math.max(4, ...report.entries.map((entry) => entry.key.length));
|
|
272
|
+
for (const entry of report.entries) {
|
|
273
|
+
const shown = !entry.set ? "missing" : entry.secret ? "set" : entry.value;
|
|
274
|
+
lines.push(` ${entry.set ? "✓" : "✗"} ${entry.key.padEnd(width)} ${shown}${entry.source ? ` [${entry.source}]` : ""}`);
|
|
275
|
+
}
|
|
276
|
+
for (const problem of report.problems) lines.push(` ${problem.level === "error" ? "ERROR" : "WARN "} ${problem.message}`);
|
|
277
|
+
if (report.ok) lines.push(" ready: the daemon can start");
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|