elanous 0.2.16 → 0.2.17
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-1e962b31d1f11081.js → page-7110f732f45bf944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8c2d9526164f5d09.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-76270efaee638f17.js → page-ab93b240f687d69a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-dce686e582c23937.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-d3d53b146416e422.js → page-8b08cafad27759c9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-5b042736aa113756.js → page-166745e51ac0c7b0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-acb749f1e432bcf3.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-31fa1ce4c4aff9ae.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-0f5cd57ca928f42b.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-1d824510df025d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-59f03fda76c03d22.js → page-81a4e7f8dbfe74cb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-27d71f131acd9320.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-d4aef67abf264874.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-1ea64acadd704ace.js → page-09461366af82ef13.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-30c2073bfc21c004.js → page-ea90a8ac73ce2749.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/src/agent-mission/driver.ts +53 -10
- package/src/autopilot/track-agent.ts +5 -3
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/harness/harness-cli-command.ts +142 -10
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/index.ts +57 -8
- package/src/intake-plane/digest.ts +39 -4
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/seat-loop/seat-loop.ts +95 -31
- package/src/self-dev/ask-launch-flow.ts +26 -7
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +88 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +19 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +151 -29
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/user-config.ts +25 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-fe1759f94003b32b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-9afdee66b820c76e.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// - 증거 게이트(doc 존재 / tsc 0 / test pass)로 완료 판정 후 commit
|
|
9
9
|
// 재사용: startPty(Bun 네이티브 PTY)·createWorktree·streamLLM·worktreeHasChanges·commitWorktree.
|
|
10
10
|
import { execFileSync, spawnSync, spawn as spawnChild } from 'node:child_process';
|
|
11
|
-
import { homedir } from 'node:os';
|
|
12
|
-
import { existsSync, writeFileSync, mkdirSync, readdirSync, readFileSync, statSync, readlinkSync, realpathSync, lstatSync, unlinkSync } from 'node:fs';
|
|
11
|
+
import { homedir, tmpdir } from 'node:os';
|
|
12
|
+
import { existsSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, readdirSync, readFileSync, statSync, readlinkSync, realpathSync, lstatSync, unlinkSync } from 'node:fs';
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import { join, relative, resolve, isAbsolute, sep } from 'node:path';
|
|
15
15
|
import { startPty, mintPtyId, onPtyEvent, type PtyHandle, type StartOpts } from '../pty-shell/registry.js';
|
|
@@ -18,7 +18,7 @@ import { NESTED_AGENT_ENV_BLOCKLIST } from '../agent/nested-agent-env.js';
|
|
|
18
18
|
import { getNestDepth } from '../agent/nest-depth.js';
|
|
19
19
|
import { gateNestedElanousLaunch, nestedDepthCodexArgs, readNestedElanousDepth } from '../harness/nested-elanous-policy.js';
|
|
20
20
|
import { createWorktree, resolveMainRepoRoot, type CreateWorktreeOpts } from '../git-fs/worktree.js';
|
|
21
|
-
import { configuredWorktreeRoot } from '../user-config.js';
|
|
21
|
+
import { configuredWorktreeRoot, getUserConfig } from '../user-config.js';
|
|
22
22
|
import { worktreeHasChanges, commitWorktree, changedFiles } from '../self-implement/seams.js';
|
|
23
23
|
import { streamLLM, type LLMMessage } from '../llm.js';
|
|
24
24
|
import { debug } from '../debug/log.js';
|
|
@@ -330,6 +330,27 @@ export function resolveBackendSpawn(
|
|
|
330
330
|
return { cmd: backend.cmd, args, env, unsetEnv, nestedEnvRemovedCount, forcedEnv };
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
/** Claude's strict MCP override is per-child; never edit the user's Claude settings. */
|
|
334
|
+
export function prepareClaudeChildSpawn(
|
|
335
|
+
spawn: ReturnType<typeof resolveBackendSpawn>,
|
|
336
|
+
options: { readonly backendName: string; readonly depth: number; readonly allowNestedElanous: boolean; readonly tempRoot?: string },
|
|
337
|
+
): { readonly args: string[]; readonly cleanup: () => void; readonly configPath?: string } {
|
|
338
|
+
// Backend identity, not the command string — an absolute-path claude binary is still the Claude backend.
|
|
339
|
+
if (options.backendName !== 'claude' || (options.depth === 0 && options.allowNestedElanous)) {
|
|
340
|
+
return { args: [...spawn.args], cleanup: () => {} };
|
|
341
|
+
}
|
|
342
|
+
const dir = mkdtempSync(join(options.tempRoot ?? tmpdir(), 'elanous-claude-mcp-'));
|
|
343
|
+
try {
|
|
344
|
+
const configPath = join(dir, 'mcp.json');
|
|
345
|
+
writeFileSync(configPath, JSON.stringify({ mcpServers: {} }), { mode: 0o600 });
|
|
346
|
+
return { args: [...spawn.args, '--strict-mcp-config', '--mcp-config', configPath], configPath,
|
|
347
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
348
|
+
} catch (error) {
|
|
349
|
+
rmSync(dir, { recursive: true, force: true });
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
333
354
|
/**
|
|
334
355
|
* Refuse a requested nested elanous launch when this backend cannot carry the depth marker.
|
|
335
356
|
* Unset depth is 0. A normal mission spawn (`nestedLaunchRequested: false`) is never refused here.
|
|
@@ -528,6 +549,8 @@ export interface AgentMissionSpec {
|
|
|
528
549
|
* Absent means an ordinary mission spawn — never refused for a missing marker.
|
|
529
550
|
*/
|
|
530
551
|
nestedElanousLaunch?: boolean;
|
|
552
|
+
/** Explicit depth-0 allow from the caller (e.g. `--nested-elanous allow`). Ignored at depth >= 1. Config `harness.nestedElanous` also counts at depth 0. */
|
|
553
|
+
nestedElanousAllow?: boolean;
|
|
531
554
|
/** Concurrent missions may share one decision stream alias without sharing a PTY or worktree. */
|
|
532
555
|
decisionMissionId?: string;
|
|
533
556
|
/** ⭐⭐ 그 브랜치를 이미 쥔 «소유» 워크트리가 있으면 지우지 말고 그대로 재사용하라고 요청한다
|
|
@@ -1574,6 +1597,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1574
1597
|
const nestedRefusal = refuseNestedElanousOnBackend({
|
|
1575
1598
|
backendName: backend.name,
|
|
1576
1599
|
nestedLaunchRequested: spec.nestedElanousLaunch === true,
|
|
1600
|
+
...(spec.nestedElanousAllow === true ? { allow: true } : {}),
|
|
1601
|
+
...(getUserConfig().harness?.nestedElanous === 'allow' ? { configAllow: true } : {}),
|
|
1577
1602
|
env: process.env,
|
|
1578
1603
|
});
|
|
1579
1604
|
if (nestedRefusal.refused) {
|
|
@@ -1691,12 +1716,16 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1691
1716
|
evidencePath: finalEv.path, evidenceSatisfied: true, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
|
|
1692
1717
|
}
|
|
1693
1718
|
env.TERM = 'xterm-256color';
|
|
1719
|
+
// Claude's per-child MCP override is held until the PTY exits, not until the control loop returns.
|
|
1720
|
+
const claudeChild = backend.name === 'claude'
|
|
1721
|
+
? prepareClaudeChildSpawn(spawnParams, { backendName: backend.name, depth: readNestedElanousDepth(process.env), allowNestedElanous: spec.nestedElanousAllow === true || getUserConfig().harness?.nestedElanous === 'allow' })
|
|
1722
|
+
: undefined;
|
|
1694
1723
|
// ⭐run-identity — runId was resolved before either headless or PTY spawn.
|
|
1695
|
-
debug.log('agent-mission', 'spawn', { agent: backend.name, cmd: `${spawnParams.cmd} ${spawnParams.args.join(' ')}`, scrubbed: backend.scrubEnv ?? [], nestedEnvRemovedCount: spawnParams.nestedEnvRemovedCount, runId });
|
|
1724
|
+
debug.log('agent-mission', 'spawn', { agent: backend.name, cmd: `${spawnParams.cmd} ${(claudeChild?.args ?? spawnParams.args).join(' ')}`, scrubbed: backend.scrubEnv ?? [], nestedEnvRemovedCount: spawnParams.nestedEnvRemovedCount, runId });
|
|
1696
1725
|
// PTY 정체성 — kind(=backend)·nickname(goto 로 나중 접근)·accessMode='auto'(헤드리스 자율·brain 이 write 소유).
|
|
1697
1726
|
const ptyOpts = buildAgentMissionPtySpawnOptions({
|
|
1698
1727
|
backend,
|
|
1699
|
-
spawn: { cmd: spawnParams.cmd, args: spawnParams.args, env, unsetEnv: spawnParams.unsetEnv },
|
|
1728
|
+
spawn: { cmd: spawnParams.cmd, args: claudeChild?.args ?? spawnParams.args, env, unsetEnv: spawnParams.unsetEnv },
|
|
1700
1729
|
workdir: wt.path,
|
|
1701
1730
|
nickname: spec.nickname ?? spec.branch ?? backend.name,
|
|
1702
1731
|
});
|
|
@@ -1706,6 +1735,11 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1706
1735
|
let usageReemitted = false;
|
|
1707
1736
|
let childSessionId: string | undefined;
|
|
1708
1737
|
let sessionOutput = '';
|
|
1738
|
+
let claudeCleanupTimer: ReturnType<typeof setInterval> | undefined;
|
|
1739
|
+
const cleanupClaude = (): void => {
|
|
1740
|
+
if (claudeCleanupTimer) clearInterval(claudeCleanupTimer);
|
|
1741
|
+
claudeChild?.cleanup();
|
|
1742
|
+
};
|
|
1709
1743
|
const offLive = onPtyEvent((ev) => {
|
|
1710
1744
|
if (ev.id !== ptyOpts.id) return;
|
|
1711
1745
|
if (ev.type === 'output') {
|
|
@@ -1718,6 +1752,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1718
1752
|
if (ev.type === 'exit') {
|
|
1719
1753
|
offLive();
|
|
1720
1754
|
if (liveTimer) clearInterval(liveTimer);
|
|
1755
|
+
cleanupClaude();
|
|
1721
1756
|
}
|
|
1722
1757
|
if (ev.type === 'exit' && backend.name === 'codex' && !usageReemitted) {
|
|
1723
1758
|
usageReemitted = true;
|
|
@@ -1740,9 +1775,17 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1740
1775
|
}
|
|
1741
1776
|
});
|
|
1742
1777
|
let h: PtyHandle;
|
|
1743
|
-
if (deps.isMissionAborted?.()) { offLive(); throw new Error('MISSION_ABORTED'); }
|
|
1778
|
+
if (deps.isMissionAborted?.()) { offLive(); cleanupClaude(); throw new Error('MISSION_ABORTED'); }
|
|
1744
1779
|
try { h = spawnPty(ptyOpts); }
|
|
1745
|
-
catch (error) { offLive(); throw error; }
|
|
1780
|
+
catch (error) { offLive(); cleanupClaude(); throw error; }
|
|
1781
|
+
if (!h.isAlive()) cleanupClaude();
|
|
1782
|
+
else if (claudeChild) {
|
|
1783
|
+
// A test-injected PTY may not emit registry events; keep the config until its handle exits.
|
|
1784
|
+
claudeCleanupTimer = setInterval(() => {
|
|
1785
|
+
if (!h.isAlive()) cleanupClaude();
|
|
1786
|
+
}, 1000);
|
|
1787
|
+
claudeCleanupTimer.unref();
|
|
1788
|
+
}
|
|
1746
1789
|
if (backend.name === 'codex') observeExternalEvent({ origin: 'codex-agent-mission', kind: 'started',
|
|
1747
1790
|
summary: 'Codex agent mission started', source: `elanous://agent-mission/${encodeURIComponent(runId)}/${encodeURIComponent(h.id)}` });
|
|
1748
1791
|
let contextFinished = false;
|
|
@@ -1753,8 +1796,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1753
1796
|
summary: `Codex agent mission ${ok ? 'completed' : 'failed'}`,
|
|
1754
1797
|
source: `elanous://agent-mission/${encodeURIComponent(runId)}/${encodeURIComponent(h.id)}` });
|
|
1755
1798
|
};
|
|
1756
|
-
deps.onPtyStarted?.(h, () => { if (liveTimer) clearInterval(liveTimer); offLive(); finishContext(false); });
|
|
1757
|
-
if (deps.isMissionAborted?.()) { finishContext(false); return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
|
|
1799
|
+
deps.onPtyStarted?.(h, () => { if (liveTimer) clearInterval(liveTimer); offLive(); finishContext(false); cleanupClaude(); });
|
|
1800
|
+
if (deps.isMissionAborted?.()) { finishContext(false); cleanupClaude(); return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
|
|
1758
1801
|
rounds: 0, evidencePath: null, committed: false, usedOmniCrawl: false, detail: 'aborted' }; }
|
|
1759
1802
|
const executorRef = buildExecutorPtyRef({
|
|
1760
1803
|
ptyId: h.id,
|
|
@@ -2275,7 +2318,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
2275
2318
|
ptyId: h.id, webUrl: webAddress.webUrl, driveVerdict: driveVerdict.kind,
|
|
2276
2319
|
detail: recoverExhausted ? control.termination.reason : driveVerdict.kind === 'done-but-failed' ? `미완(DRIVE-OK: ${driveVerdict.reason})` : driveVerdict.kind === 'success-unverified' && finalEv.ok ? '완료(증거)' : done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
|
|
2277
2320
|
};
|
|
2278
|
-
} finally { stopLive(); finishContext(false); }
|
|
2321
|
+
} finally { stopLive(); finishContext(false); if (!h.isAlive() || deps.isMissionAborted?.()) cleanupClaude(); }
|
|
2279
2322
|
}
|
|
2280
2323
|
|
|
2281
2324
|
/** @deprecated codex 특정 이름 — runAgentMission 을 쓰라(codex 는 기본 backend). */
|
|
@@ -47,7 +47,9 @@ const runCli = async (args: string[]): Promise<{ stdout: string }> => {
|
|
|
47
47
|
const { argv, env } = universeLaunch(args);
|
|
48
48
|
return exec('bun', argv, { cwd: repoDir(), env, timeout: 120_000, maxBuffer: 1024 * 1024, encoding: 'utf8' });
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
// A work topic containing a verb is not necessarily an instruction; the guard matches executable requests.
|
|
51
|
+
// Security words (secret · credential · force · 자격 · 비밀 · 강제) stay blocked anywhere — topic or request (UX harvest).
|
|
52
|
+
const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|pay|purchase|sudo)\b(?=\s+(?:the\s+(?:branch|site|app|release|file|record)\b|(?:this\s+)?branch\b|(?:to|into)\s+main\b|main\b|now\b|(?:site|app)\b))|\bcut\s+a\s+release\b|\brm\s+-rf\b|\bconfig\s+set\b|\b(?:secret|credential|force|sudo)\b|자격|비밀|강제|(?:^|\s)--prod\b|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|강제)(?:해라|하세요|해줘|하라|해\s*주세요|하시오)(?!\S)|(?:^\s*|후\s*)(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*$|\s*후\s*)|(?:^\s*운영\s*(?:배포|재시작)(?=\s*$))|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*(?:해|실행|진행)(?:\s|$))/iu;
|
|
51
53
|
export const TRACK_AGENT_FORBIDDEN_ACTION_REGEX = new RegExp(forbiddenActionRegex.source, forbiddenActionRegex.flags);
|
|
52
54
|
let running = 0;
|
|
53
55
|
|
|
@@ -101,7 +103,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
|
|
|
101
103
|
const values = decision as Record<string, unknown>;
|
|
102
104
|
if (!['say', 'hold'].includes(String(values.action)) || (values.reason !== undefined && typeof values.reason !== 'string')
|
|
103
105
|
|| Object.keys(values).some((key) => !['action', 'reason'].includes(key))) throw new Error('invalid decision');
|
|
104
|
-
if (typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
|
|
106
|
+
if (values.action === 'say' && typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
|
|
105
107
|
return decision as TrackAgentDecision;
|
|
106
108
|
} catch (error) {
|
|
107
109
|
log('decision-failed', { track: input.track, taskId: input.taskId, error: redactSecretText(String(error)) });
|
|
@@ -111,7 +113,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
|
|
|
111
113
|
|
|
112
114
|
export async function executeTrackAction(decision: TrackAgentDecision, input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentResult> {
|
|
113
115
|
if (decision.action !== 'say') return { status: 'held', detail: decision.reason ?? '사람 확인' };
|
|
114
|
-
if (forbiddenActionRegex.test(
|
|
116
|
+
if (forbiddenActionRegex.test(input.prompt)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
|
|
115
117
|
if (!findTrack(input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
|
|
116
118
|
try {
|
|
117
119
|
const checkBudget = deps.checkBudget ?? (async () => {
|
package/src/cli/autopilot-cli.ts
CHANGED
|
@@ -7,9 +7,75 @@ import { writeStdoutJson } from './stdout-json.js';
|
|
|
7
7
|
// AutopilotLoopDriver against the mission, prints agent deltas on
|
|
8
8
|
// stdout + envelope/termination summary on stderr.
|
|
9
9
|
export function registerAutopilotCommands(program: Command): void {
|
|
10
|
+
// Explicit runtime entry for a selected action; the task chooser can invoke this entry.
|
|
10
11
|
const autopilotCmd = program
|
|
11
12
|
.command('autopilot')
|
|
12
13
|
.description('Mission-driven autopilot — runs an ACP agent against a single mission with safety + budget guards');
|
|
14
|
+
autopilotCmd.command('task-agent-action <actionJson>')
|
|
15
|
+
.description('Execute a selected task-agent action; defaults to shadow')
|
|
16
|
+
.action(async (actionJson: string) => {
|
|
17
|
+
const { executeNextAction } = await import('../task-agent/actions.js');
|
|
18
|
+
const { getUserConfig } = await import('../user-config.js');
|
|
19
|
+
const { spawnSync } = await import('node:child_process');
|
|
20
|
+
const { readFileSync, writeFileSync, mkdirSync } = await import('node:fs');
|
|
21
|
+
const { join, dirname } = await import('node:path');
|
|
22
|
+
const { debug } = await import('../debug/log.js');
|
|
23
|
+
const { withFileLockSync } = await import('../storage/file-lock.js');
|
|
24
|
+
const config = getUserConfig() as ReturnType<typeof getUserConfig> & { taskAgent?: { mode?: string } };
|
|
25
|
+
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
26
|
+
const statePath = join(effectiveInstanceRoot(), 'task-agent-actions.json');
|
|
27
|
+
let state: { landingDay?: string; landingsToday?: number; failureCounts?: Record<string, number> } = {};
|
|
28
|
+
try { state = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first run */ }
|
|
29
|
+
const action = JSON.parse(actionJson) as import('../task-agent/actions.js').NextAction;
|
|
30
|
+
const deps: import('../task-agent/actions.js').ActionDeps = {
|
|
31
|
+
...state,
|
|
32
|
+
failureCounts: state.failureCounts ?? {},
|
|
33
|
+
saveState: () => {
|
|
34
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
35
|
+
// 실패 횟수는 incrementFailure 가 잠금 안에서 키별로 더한다 — 여기선 덮어쓰지 않는다.
|
|
36
|
+
},
|
|
37
|
+
mode: config.taskAgent?.mode,
|
|
38
|
+
// 하루 착지 칸은 상태 파일 잠금 안에서 확보·반환한다 — 두 CLI 프로세스가 같은 잔여 칸을 쓰지 않게.
|
|
39
|
+
reserveLanding: (day, limit) => withFileLockSync(`${statePath}.lock`, () => {
|
|
40
|
+
let disk: typeof state = {};
|
|
41
|
+
try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first */ }
|
|
42
|
+
const used = disk.landingDay === day ? disk.landingsToday ?? 0 : 0;
|
|
43
|
+
if (used >= limit) return false;
|
|
44
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
45
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, landingDay: day, landingsToday: used + 1 }));
|
|
46
|
+
deps.landingDay = day; deps.landingsToday = used + 1;
|
|
47
|
+
return true;
|
|
48
|
+
}),
|
|
49
|
+
incrementFailure: (key) => withFileLockSync(`${statePath}.lock`, () => {
|
|
50
|
+
let disk: typeof state = {};
|
|
51
|
+
try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first */ }
|
|
52
|
+
const counts = { ...(disk.failureCounts ?? {}) };
|
|
53
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
54
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
55
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, failureCounts: counts }));
|
|
56
|
+
return counts[key]!;
|
|
57
|
+
}),
|
|
58
|
+
releaseLanding: (day) => withFileLockSync(`${statePath}.lock`, () => {
|
|
59
|
+
let disk: typeof state = {};
|
|
60
|
+
try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { return; }
|
|
61
|
+
if (disk.landingDay !== day || !disk.landingsToday) return;
|
|
62
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, landingsToday: disk.landingsToday - 1 }));
|
|
63
|
+
deps.landingsToday = disk.landingsToday - 1;
|
|
64
|
+
}),
|
|
65
|
+
observe: (_event, data) => debug.log('task-agent', 'action', data),
|
|
66
|
+
command: async args => {
|
|
67
|
+
const p = args[0] === 'gh'
|
|
68
|
+
// PR 조회는 착지할 작업 트리에서 — 다른 저장소에서 불러도 엉뚱한 PR 을 읽지 않게.
|
|
69
|
+
? spawnSync('gh', args.slice(1), { encoding: 'utf8', ...(action.cwd ? { cwd: action.cwd } : {}) })
|
|
70
|
+
// 지금 돌고 있는 진입점으로 부른다 — 저장소 밖(설치본)에서 불러도 같은 elanous 를 쓴다.
|
|
71
|
+
: spawnSync(process.execPath, [process.argv[1]!, ...(process.argv.includes('--test') ? ['--test'] : []), ...args], { encoding: 'utf8' });
|
|
72
|
+
return { status: p.status ?? 1, stdout: p.stdout ?? '', stderr: p.stderr ?? '' };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const result = await executeNextAction(action, deps);
|
|
76
|
+
if (config.taskAgent?.mode === 'live') deps.saveState?.();
|
|
77
|
+
process.stdout.write(`${result}\n`);
|
|
78
|
+
});
|
|
13
79
|
|
|
14
80
|
autopilotCmd
|
|
15
81
|
.command('run <mission...>')
|
package/src/cli/decisions-cli.ts
CHANGED
|
@@ -53,6 +53,7 @@ export function formatDecisionDetail(e: DecisionEntry): string {
|
|
|
53
53
|
...(e.note ? [`메모: ${e.note}`] : [])] : []),
|
|
54
54
|
...(e.withdrawnAt ? [`철회: ${kst(e.withdrawnAt)} · ${e.withdrawReason}`] : []),
|
|
55
55
|
'이력:', ...e.history.map(h => ` ${h.type} · ${h.at ? kst(h.at) : '시각 미상'} · ${h.by}${h.choice ? ` · ${h.choice}` : ''}${h.reason ? ` · ${h.reason}` : ''} · ${h.version ? version(h.version) : '판 미상'}`),
|
|
56
|
+
...(e.pendingQuestion ? [`판단 재료:\n${e.pendingQuestion}`] : []),
|
|
56
57
|
...(e.refs?.length ? [`참조: ${e.refs.join(' · ')}`] : [])].join('\n');
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -86,6 +87,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
86
87
|
root.command('raise').description('SCQA·선택지와 권고로 결정 항목을 올린다')
|
|
87
88
|
.requiredOption('--title <text>').requiredOption('--category <category>')
|
|
88
89
|
.requiredOption('--s <text>').requiredOption('--c <text>').option('--q <text>').option('--a <text>')
|
|
90
|
+
.option('--pending-question <text>', '판단 재료 전문 (무엇·지금까지·재측·선택지별 결과·권고·확신·그냥 두면·근거)')
|
|
89
91
|
.requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
|
|
90
92
|
.option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
|
|
91
93
|
.option('--xcheck <SEAT:메모>', '교차 확인 (반복)', repeat, [] as string[])
|
|
@@ -96,7 +98,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
96
98
|
.option('--ref <url>', '참조 (반복)', repeat, [] as string[])
|
|
97
99
|
.option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
|
|
98
100
|
.option('--json').option('--hq-override')
|
|
99
|
-
.action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
101
|
+
.action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; pendingQuestion?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
100
102
|
if (o.run !== undefined && o.resumeQuestion === undefined) throw new Error('--run requires --resume-question');
|
|
101
103
|
if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
|
|
102
104
|
if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
|
|
@@ -111,6 +113,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
111
113
|
const who = agent(o.agent);
|
|
112
114
|
if (!mayWrite('raise', o.hqOverride)) return;
|
|
113
115
|
const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
|
|
116
|
+
...(o.pendingQuestion ? { pendingQuestion: o.pendingQuestion } : {}),
|
|
114
117
|
...(crossCheck.length ? { crossCheck } : { crossCheckSkipped: o.noXcheck ?? 'missing' }),
|
|
115
118
|
...(o.alternative !== undefined ? { alternative: o.alternative } : {}), ...(o.dissent !== undefined ? { dissent: o.dissent } : {}),
|
|
116
119
|
recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
|
package/src/cli/intake-cli.ts
CHANGED
|
@@ -102,7 +102,7 @@ export async function runIntakeDigestCli(
|
|
|
102
102
|
}
|
|
103
103
|
const d = (deps.buildDigest ?? buildIntakeDigest)(root, day);
|
|
104
104
|
if (opts.telegram) {
|
|
105
|
-
if (!d.absorbed.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
|
|
105
|
+
if (!d.absorbed.length && !d.news?.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
|
|
106
106
|
const text = renderDigestTelegram(d, { ...(opts.vault ? { vaultRoot: opts.vault } : {}), ...(opts.note ? { notePath: opts.note } : {}) });
|
|
107
107
|
const sent = deps.sendTelegram
|
|
108
108
|
? await deps.sendTelegram(text)
|
package/src/cli/pr-cli.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface PrLandOpts {
|
|
|
75
75
|
excludeActiveRunFiles?: boolean;
|
|
76
76
|
excludeActiveRunSources?: boolean;
|
|
77
77
|
landReason?: string;
|
|
78
|
+
/** Explicit reason for bypassing the landing freeze. */
|
|
79
|
+
forceFreeze?: string;
|
|
78
80
|
/** Land even though changed files would leak into the public export (the reason goes in --land-reason). */
|
|
79
81
|
allowPublicLeak?: boolean;
|
|
80
82
|
}
|
|
@@ -130,6 +132,8 @@ export interface PrLandDeps {
|
|
|
130
132
|
overlapConfirmChannels?: ConfirmChannel[];
|
|
131
133
|
overlapConfirmTimeoutMs?: number;
|
|
132
134
|
decideOverlapLanding?: () => OverlapLandingDecision | Promise<OverlapLandingDecision>;
|
|
135
|
+
/** Operational freeze authority (isolated in tests). */
|
|
136
|
+
prodFreezeRoot?: string;
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
export const OVERLAP_DECISION_PROMPT = '겹친 파일이 있습니다. 이번 착지를 그대로 병합할까요?';
|
|
@@ -1132,6 +1136,10 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1132
1136
|
out.error('✗ body: --body와 --body-file은 함께 사용할 수 없습니다.');
|
|
1133
1137
|
return 1;
|
|
1134
1138
|
}
|
|
1139
|
+
if (opts.forceFreeze !== undefined && !opts.forceFreeze.trim()) {
|
|
1140
|
+
out.error('✗ freeze: --force-freeze 에는 이유가 필요합니다.');
|
|
1141
|
+
return 1;
|
|
1142
|
+
}
|
|
1135
1143
|
if (opts.allowPublicLeak && !opts.landReason?.trim()) {
|
|
1136
1144
|
out.error('✗ public-export-leak: --allow-public-leak 에는 --land-reason "<이유>" 가 필요합니다.');
|
|
1137
1145
|
return 1;
|
|
@@ -1649,7 +1657,8 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1649
1657
|
const headCommit = landedHead.ok && /^[a-f0-9]{40}$/i.test(landedHead.out.trim()) ? landedHead.out.trim() : undefined;
|
|
1650
1658
|
const validPr = Number.isSafeInteger(prNumber) && prNumber > 0;
|
|
1651
1659
|
const heldEntry = validPr && headCommit ? { prNumber, headCommit, repoRoot: cwd } : null;
|
|
1652
|
-
const admission = admitLandingMerge(heldEntry, undefined, {}, validPr ? { prNumber, repoRoot: cwd, ...(headCommit ? { headCommit } : {}) } : undefined
|
|
1660
|
+
const admission = admitLandingMerge(heldEntry, undefined, {}, validPr ? { prNumber, repoRoot: cwd, ...(headCommit ? { headCommit } : {}) } : undefined,
|
|
1661
|
+
{ prodFreezeRoot: deps.prodFreezeRoot, forceReason: opts.forceFreeze });
|
|
1653
1662
|
if (admission.kind === 'held' && !heldEntry) {
|
|
1654
1663
|
out.error(`✗ freeze: ready PR의 검증 HEAD/번호를 보존하지 못했습니다: ${upsert.url}`);
|
|
1655
1664
|
return 1;
|
|
@@ -1712,6 +1721,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1712
1721
|
}
|
|
1713
1722
|
return 1;
|
|
1714
1723
|
}
|
|
1724
|
+
if (opts.forceFreeze) debug.log('release-loop.freeze', 'force-merge', { pr: prNumber, reason: opts.forceFreeze.trim() });
|
|
1715
1725
|
record('merge', true, { result: merged.kind, ...(merged.remoteBranchDeletion ? { remoteBranchDeletion: merged.remoteBranchDeletion.detail } : {}) });
|
|
1716
1726
|
if (merged.remoteBranchDeletion) {
|
|
1717
1727
|
out.log(`⚠ remote branch deletion: ${merged.remoteBranchDeletion.detail}`);
|
|
@@ -1763,6 +1773,7 @@ export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queu
|
|
|
1763
1773
|
.option('--exclude-active-run-files', '도는 런과 겹친 파일을 이번 착지에서 제외')
|
|
1764
1774
|
.option('--exclude-active-run-sources', '⚠ 위험: --exclude-active-run-files 가 골 문서가 아닌 파일(소스)을 빼도 착지를 강행')
|
|
1765
1775
|
.option('--land-reason <text>', '권고를 보고도 지금 내는 이유(예: 남이 기다리는 차단 해제)')
|
|
1776
|
+
.option('--force-freeze <reason>', '동결을 넘겨 병합할 명시적 이유(필수)')
|
|
1766
1777
|
.option('--allow-public-leak', '바뀐 파일이 공개본에 사적 흔적을 실어도 착지(이유는 --land-reason)')
|
|
1767
1778
|
.action(async (opts: PrLandOpts) => {
|
|
1768
1779
|
// ⛔⭐⭐⭐ **관측 sink 를 먼저 건다** — 라이브 도그푸드가 잡은 결함(2026-08-03).
|
package/src/cli/provider-cli.ts
CHANGED
|
@@ -392,6 +392,20 @@ export function registerProviderCommands(program: Command): void {
|
|
|
392
392
|
console.log(' 확인: bun bin/elanous.mjs provider codex account list · bun bin/elanous.mjs usage');
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
+
// POD-TOKEN-PREREFRESH (10-06): 사람이 두 번 손으로 한 «백업 → refresh → 원자적 0600 쓰기 → import» 를 한 명령으로.
|
|
396
|
+
// ⛔ 토큰 값은 안 찍는다 — 남은 시간 전→후만. ⛔ default(~/.codex · 대표 기본 계정)는 거부.
|
|
397
|
+
accountCmd
|
|
398
|
+
.command('refresh <name>')
|
|
399
|
+
.description('그 계정의 codex 토큰을 호스트에서 갱신해 정본 스토어로 반영한다 (⛔ default 거부 · 토큰 값은 안 찍는다 · ⚠️ refresh 토큰은 갱신마다 회전 — 본부 한 곳에서만)')
|
|
400
|
+
.action(async (name: string) => {
|
|
401
|
+
const { refreshCodexAccountHome } = await import('../oauth/codex.js');
|
|
402
|
+
const r = await refreshCodexAccountHome(name);
|
|
403
|
+
if (!r.ok) { console.error(`갱신 실패(${r.kind}): ${r.message}`); process.exitCode = 1; return; }
|
|
404
|
+
const h = (v: number | null) => (v === null ? '?' : `${v}h`);
|
|
405
|
+
console.log(`✅ ${name} 갱신 — 남은 시간 ${h(r.beforeH)} → ${h(r.afterH)} · storeKey=${r.storeKey}`);
|
|
406
|
+
console.log(` 백업: ${r.backupPath}`);
|
|
407
|
+
});
|
|
408
|
+
|
|
395
409
|
const resetCreditsCmd = codexCmd.command('reset-credits').description('리셋 크레딧 — 조회 · 관측 · 사용(⛔ 사용은 되돌릴 수 없다)');
|
|
396
410
|
|
|
397
411
|
resetCreditsCmd
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type EdgeRailDecision = 'hold' | 'canary-install' | 'promote-all' | 'rollback' | 'wait-quiet';
|
|
2
|
+
export interface EdgeRailPlan { decision: EdgeRailDecision; reason: string }
|
|
3
|
+
export interface EdgeRailInput {
|
|
4
|
+
now: Date;
|
|
5
|
+
mainCommit: string;
|
|
6
|
+
installed: string;
|
|
7
|
+
seat: string;
|
|
8
|
+
canary: { seat: string; okRuns: number; startedAt: string } | null;
|
|
9
|
+
failureRate: { before: number; after: number; samples: number } | null;
|
|
10
|
+
quietWindow: boolean;
|
|
11
|
+
canaryOkRuns?: number;
|
|
12
|
+
failureMultiplier?: number;
|
|
13
|
+
minSamples?: number;
|
|
14
|
+
promoted?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** No I/O: unknown health, insufficient samples and unknown revision never trigger a cutover. */
|
|
18
|
+
export function planEdgeRail(input: EdgeRailInput): EdgeRailPlan {
|
|
19
|
+
const { mainCommit, installed, seat, canary, failureRate, quietWindow } = input;
|
|
20
|
+
const okRuns = input.canaryOkRuns ?? 3;
|
|
21
|
+
const multiplier = input.failureMultiplier ?? 2;
|
|
22
|
+
const minSamples = input.minSamples ?? 30;
|
|
23
|
+
if (!Number.isFinite(input.now.getTime()) || !/^[a-f0-9]{12,40}$/i.test(mainCommit) || !/^[a-f0-9]{12,40}$/i.test(installed)
|
|
24
|
+
|| !Number.isSafeInteger(okRuns) || okRuns < 1 || !Number.isFinite(multiplier) || multiplier <= 1
|
|
25
|
+
|| !Number.isSafeInteger(minSamples) || minSamples < 1) return { decision: 'hold', reason: 'invalid or unknown rollout input' };
|
|
26
|
+
const same = mainCommit.startsWith(installed) || installed.startsWith(mainCommit);
|
|
27
|
+
if (canary && Date.parse(canary.startedAt) > input.now.getTime()) return { decision: 'hold', reason: 'canary started in the future' };
|
|
28
|
+
if (canary && (canary.seat !== 'OP' || !Number.isSafeInteger(canary.okRuns) || canary.okRuns < 0
|
|
29
|
+
|| !Number.isFinite(Date.parse(canary.startedAt)))) return { decision: 'hold', reason: 'invalid canary state' };
|
|
30
|
+
if (failureRate) {
|
|
31
|
+
if (![failureRate.before, failureRate.after, failureRate.samples].every(Number.isFinite)
|
|
32
|
+
|| failureRate.before < 0 || failureRate.after < 0 || failureRate.before > 1 || failureRate.after > 1
|
|
33
|
+
|| !Number.isSafeInteger(failureRate.samples) || failureRate.samples < 0) return { decision: 'hold', reason: 'invalid failure-rate measurement' };
|
|
34
|
+
if (failureRate.samples < minSamples) return { decision: 'hold', reason: `failure-rate samples ${failureRate.samples} < ${minSamples}` };
|
|
35
|
+
if (same && canary && failureRate.after > 0 && failureRate.after >= failureRate.before * multiplier) return quietWindow
|
|
36
|
+
? { decision: 'rollback', reason: `failure rate ${failureRate.after} >= ${multiplier}x ${failureRate.before}` }
|
|
37
|
+
: { decision: 'wait-quiet', reason: 'rollback requires quiet window for resident restart' };
|
|
38
|
+
}
|
|
39
|
+
if (canary && canary.okRuns < okRuns) return { decision: 'hold', reason: `OP canary ${canary.okRuns}/${okRuns} successful runs` };
|
|
40
|
+
if (same) return { decision: 'hold', reason: 'installed commit already matches main' };
|
|
41
|
+
if (input.promoted && canary && seat !== 'OP') return { decision: 'hold', reason: 'fleet promotion already completed' };
|
|
42
|
+
if (!canary && seat !== 'OP') return { decision: 'hold', reason: 'OP canary has not installed' };
|
|
43
|
+
if (!quietWindow) return { decision: 'wait-quiet', reason: 'resident restart requires quiet window' };
|
|
44
|
+
if (!canary) return { decision: 'canary-install', reason: 'install OP canary first' };
|
|
45
|
+
return { decision: 'promote-all', reason: `OP canary passed ${canary.okRuns}/${okRuns} successful runs` };
|
|
46
|
+
}
|