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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { dlopen, FFIType } from 'bun:ffi';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { constants as osConstants, hostname } from 'node:os';
|
|
6
|
-
import { basename, dirname, resolve, sep } from 'node:path';
|
|
6
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
7
7
|
import { findGitDir } from '../git-fs/locate.js';
|
|
8
8
|
import { LogStore, logsDbPath } from '../mss/logging/log-store.js';
|
|
9
9
|
import { loadSelfDevRun } from '../self-dev/run-store.js';
|
|
@@ -14,7 +14,7 @@ import { archiveGoals, type GoalArchiveOptions } from '../self-implement/goal-ar
|
|
|
14
14
|
import { detectBursts, formatIncidentBurstWarning, incidentLastLines, readRunExits, recentBurst, recordRunExit } from './harness-incidents.js';
|
|
15
15
|
import { Command, Option } from 'commander';
|
|
16
16
|
import { runGitCommand } from '../git-fs/runner.js';
|
|
17
|
-
import { GOAL_TYPES, lintGoalFile, parseGoalId, parseGoalType, tracedPathReferences, type GoalType } from '../self-implement/goal-author.js';
|
|
17
|
+
import { GOAL_TYPES, lintGoalFile, parseGoalId, parseGoalType, resolveGoalAuthorGrade, tracedPathReferences, type GoalAuthorGrade, type GoalAuthorGradeSelection, type GoalType } from '../self-implement/goal-author.js';
|
|
18
18
|
import { templateForGoalType } from '../self-implement/graph-templates.js';
|
|
19
19
|
import { listRunLedgers, loadFederatedRunLedger, loadRunLedger, runLedgerDir, type RunLedgerMatch } from '../self-implement/run-ledger.js';
|
|
20
20
|
import { resolveChildLlmEffort, resolveImplementationChildModel } from '../self-dev/dev-cli.js';
|
|
@@ -33,7 +33,8 @@ import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDep
|
|
|
33
33
|
import { getUserConfig } from '../user-config.js';
|
|
34
34
|
import { resolveRepositoryName } from './repository-name.js';
|
|
35
35
|
import { installHarnessCliSinkHook } from './harness-cli-sink.js';
|
|
36
|
-
import { addHarnessQueue, listHarnessQueue, queueSeatForCwd, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps, type QueueSeat } from './harness-queue.js';
|
|
36
|
+
import { addHarnessQueue, HarnessQueueDuplicateError, harnessQueueReceiptPath, listHarnessQueue, queueSeatForCwd, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps, type QueueItem, type QueueSeat } from './harness-queue.js';
|
|
37
|
+
import { writeHarnessQueueReceipt } from './harness-queue-child.js';
|
|
37
38
|
import { parseDoorSince, queryLaunchDoors, renderLaunchDoors, type DoorTable } from './launch-stamp.js';
|
|
38
39
|
import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from './harness-substrate-default.js';
|
|
39
40
|
import { runHarnessPlanRfc } from './harness-plan-rfc.js';
|
|
@@ -110,9 +111,12 @@ export interface HarnessAskSayOptions {
|
|
|
110
111
|
supervise?: boolean;
|
|
111
112
|
supervisorSource?: HarnessSupervisorSource;
|
|
112
113
|
goalType?: GoalType;
|
|
114
|
+
authorGrade?: GoalAuthorGrade;
|
|
115
|
+
authorGradeSource?: GoalAuthorGradeSelection['source'];
|
|
113
116
|
correlation?: string;
|
|
114
117
|
/** Depth 0 only. At depth >= 1 this flag is ignored and the launch stays refused. */
|
|
115
118
|
nestedElanous?: 'allow';
|
|
119
|
+
queue?: boolean;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
export interface ResolvedHarnessSupervisor {
|
|
@@ -263,6 +267,7 @@ function registerHarnessAskSayOptions(command: Command): Command {
|
|
|
263
267
|
.option('--yes', 'git 아닌 프로젝트에 git init 및 첫 커밋을 만들도록 동의')
|
|
264
268
|
.option('--correlation <id>', '요청과 런을 잇는 불투명 correlation 값')
|
|
265
269
|
.addOption(new Option('--seat <seat>', '런 귀속 자리 (OP|TC|MK|UX)').choices(['OP', 'TC', 'MK', 'UX']))
|
|
270
|
+
.option('--no-queue', '비상시 자리별 대기열을 건너뛰고 직접 발사')
|
|
266
271
|
.addOption(new Option('--goal-type <type>', `골 종류 (${GOAL_TYPES.join('|')})`).choices([...GOAL_TYPES]))
|
|
267
272
|
.option('--force-preflight', '전제 검사 막힘을 명시 요청으로 우회(관측에 남음)')
|
|
268
273
|
.option('--force-gate', '같은 골 실행 중 막힘을 명시 요청으로 우회(관측에 남음; 예산 막힘은 우회 불가)')
|
|
@@ -302,6 +307,8 @@ function normalizeHarnessCommonOptions(opts: HarnessAskSayOptions): HarnessAskSa
|
|
|
302
307
|
...(opts.mergeByHost === true ? { mergeByHost: true } : {}),
|
|
303
308
|
...(opts.observeOnly ? { observeOnly: true } : {}),
|
|
304
309
|
...(opts.goalType !== undefined ? { goalType: opts.goalType } : {}),
|
|
310
|
+
...(opts.authorGrade !== undefined ? { authorGrade: opts.authorGrade } : {}),
|
|
311
|
+
...(opts.authorGradeSource !== undefined ? { authorGradeSource: opts.authorGradeSource } : {}),
|
|
305
312
|
...resolveHarnessSupervisor(opts),
|
|
306
313
|
...resolveNestedElanousOption(opts),
|
|
307
314
|
};
|
|
@@ -589,6 +596,16 @@ function announceLocalChildProvider(opts: unknown): void {
|
|
|
589
596
|
console.error(`child provider 요청 = ${child.childLlmProvider.trim()}${child.childLlmModel?.trim() ? `/${child.childLlmModel.trim()}` : ''} (local)`);
|
|
590
597
|
}
|
|
591
598
|
|
|
599
|
+
/** `harness.queue.directSay` (ONEDOOR-2 · OP 10-06 기본 끔): config > env ELANOUS_HARNESS_QUEUE_DIRECT_SAY > false. */
|
|
600
|
+
export function harnessQueueDirectSay(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
601
|
+
try {
|
|
602
|
+
const configured = getUserConfig().harness?.queue?.directSay;
|
|
603
|
+
if (typeof configured === 'boolean') return configured;
|
|
604
|
+
} catch { /* An unreadable config keeps the old direct path. */ }
|
|
605
|
+
const raw = env.ELANOUS_HARNESS_QUEUE_DIRECT_SAY?.trim().toLowerCase();
|
|
606
|
+
return raw === '1' || raw === 'true';
|
|
607
|
+
}
|
|
608
|
+
|
|
592
609
|
/** `harness.authorOnPod` — 명시 true 만 켠다. 읽기 실패는 끔(호스트 저작)으로 둔다. */
|
|
593
610
|
function harnessAuthorOnPod(): boolean {
|
|
594
611
|
try { return getUserConfig().harness?.authorOnPod === true; }
|
|
@@ -633,12 +650,17 @@ async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-s
|
|
|
633
650
|
let output = '';
|
|
634
651
|
let runId: string | undefined;
|
|
635
652
|
const child = opts as HarnessAskSayChildLlmOptions;
|
|
653
|
+
const podAuthorGrade = authorOnPod && entrance === 'cli-harness-say'
|
|
654
|
+
? child.authorGradeSource && child.authorGrade
|
|
655
|
+
? { grade: child.authorGrade, source: child.authorGradeSource }
|
|
656
|
+
: resolveGoalAuthorGrade(child.authorGrade, getUserConfig().harness?.authorGrade)
|
|
657
|
+
: undefined;
|
|
636
658
|
// 10-05 PODPROVIDER: say which child provider the Pod will use when the launch named one.
|
|
637
659
|
if (child.childLlmProvider?.trim()) console.error(`child provider 요청 = ${child.childLlmProvider.trim()}${child.childLlmModel?.trim() ? `/${child.childLlmModel.trim()}` : ''} (pod · 자격 없으면 발사 전 거부)`);
|
|
638
660
|
const status = await dispatchHarnessOnPod({ entrance, input, podPool: pool, ...(authorOnPod && entrance === 'cli-harness-say' ? { authorOnPod: true } : {}),
|
|
639
661
|
...(child.childLlmProvider?.trim() ? { childLlmProvider: child.childLlmProvider.trim() } : {}),
|
|
640
662
|
...(child.childLlmModel?.trim() ? { childLlmModel: child.childLlmModel.trim() } : {}),
|
|
641
|
-
...(child.childLlmEffort?.trim() ? { childLlmEffort: child.childLlmEffort.trim() } : {}), ...(dispatchRecorded ? { dispatchRecorded: true } : {}), ...(o.podMemory ? { podMemory: o.podMemory } : {}), ...((opts as HarnessAskSayChildLlmOptions).goalType ? { goalType: (opts as HarnessAskSayChildLlmOptions).goalType } : {}), ...(o.after ? { after: o.after } : {}), ...(o.autoMerge === false ? { autoMerge: false } : {}), ...(o.base ? { base: o.base } : {}), ...(o.json ? { json: true } : {}), ...(o.source ? { source: o.source } : {}), ...(o.seat ? { seat: o.seat } : {}) },
|
|
663
|
+
...(child.childLlmEffort?.trim() ? { childLlmEffort: child.childLlmEffort.trim() } : {}), ...(authorOnPod && podAuthorGrade ? { authorGrade: podAuthorGrade.grade, authorGradeSource: podAuthorGrade.source } : {}), ...(dispatchRecorded ? { dispatchRecorded: true } : {}), ...(o.podMemory ? { podMemory: o.podMemory } : {}), ...((opts as HarnessAskSayChildLlmOptions).goalType ? { goalType: (opts as HarnessAskSayChildLlmOptions).goalType } : {}), ...(o.after ? { after: o.after } : {}), ...(o.autoMerge === false ? { autoMerge: false } : {}), ...(o.base ? { base: o.base } : {}), ...(o.json ? { json: true } : {}), ...(o.source ? { source: o.source } : {}), ...(o.seat ? { seat: o.seat } : {}) },
|
|
642
664
|
{ onOutput: (text) => {
|
|
643
665
|
runId ??= harnessPodRunId(output + text);
|
|
644
666
|
output = (output + text).slice(-16_000);
|
|
@@ -716,22 +738,102 @@ async function dispatchHarnessAskSay(
|
|
|
716
738
|
},
|
|
717
739
|
dispatch: (resolved: ResolvedHarnessSubstrate, stampedOpts: HarnessAskSayChildLlmOptions & HarnessDryRunOpts) => Promise<void>,
|
|
718
740
|
gate: PreLaunchGateDeps & { readBudget?: () => Promise<BudgetDecision | 'unknown'> } = {},
|
|
741
|
+
queueDeps?: HarnessQueueDeps,
|
|
742
|
+
immediateLaunch?: HarnessQueueDeps['launch'],
|
|
743
|
+
immediateExit?: () => Promise<number>,
|
|
719
744
|
): Promise<void> {
|
|
720
745
|
await runInjectedHarnessHandler(async () => {
|
|
721
746
|
assertHarnessChildLlmModel(opts);
|
|
722
|
-
const resolved = resolveLaunchSubstrate(opts);
|
|
723
747
|
if (isHarnessDryRun(opts)) {
|
|
748
|
+
resolveLaunchSubstrate(opts);
|
|
724
749
|
printHarnessLaunchDryRun(dryRunPreview);
|
|
725
750
|
return;
|
|
726
751
|
}
|
|
727
752
|
// Resolve the launch directory once; descendants and the checkpoint retain the same identity.
|
|
728
753
|
const queued = process.env.ELANOUS_HARNESS_QUEUE_LAUNCH && process.env.ELANOUS_HARNESS_SEAT;
|
|
729
754
|
const queueSeat = queued === 'OP' || queued === 'TC' || queued === 'MK' || queued === 'UX' ? queued : undefined;
|
|
730
|
-
|
|
755
|
+
let cwdSeatMemo: { seat: QueueSeat | undefined } | undefined;
|
|
756
|
+
const cwdSeat = (): QueueSeat | undefined =>
|
|
757
|
+
(cwdSeatMemo ??= { seat: queueSeatForCwd(process.cwd(), getUserConfig().loops?.orchestrator?.seatTrees) }).seat;
|
|
758
|
+
const assigned = opts.seat ?? queueSeat ?? cwdSeat();
|
|
731
759
|
const inheritedSeat = process.env.ELANOUS_HARNESS_SEAT;
|
|
760
|
+
const directSay = harnessQueueDirectSay();
|
|
761
|
+
const shouldQueue = directSay && opts.queue !== false && !process.env.ELANOUS_HARNESS_QUEUE_LAUNCH
|
|
762
|
+
&& Boolean(opts.seat ?? cwdSeat());
|
|
763
|
+
const resolved = shouldQueue
|
|
764
|
+
? resolveHarnessSubstrate({ flag: opts as HarnessSubstrateOpts, config: getUserConfig(), env: process.env })
|
|
765
|
+
: resolveLaunchSubstrate(opts);
|
|
732
766
|
const podSeat = resolved.substrate === 'pod' && ['OP', 'TC', 'MK', 'UX'].includes(inheritedSeat ?? '')
|
|
733
767
|
? inheritedSeat as QueueSeat : undefined;
|
|
734
768
|
const stampedSeat = resolved.substrate === 'pod' ? opts.seat ?? queueSeat ?? podSeat ?? assigned : assigned;
|
|
769
|
+
if (directSay && opts.queue === false) {
|
|
770
|
+
try { debug.log('harness.queue', 'bypass', { seat: stampedSeat ?? null, reason: '--no-queue' }); }
|
|
771
|
+
catch { /* Observation must not prevent an emergency launch. */ }
|
|
772
|
+
} else if (shouldQueue && stampedSeat) {
|
|
773
|
+
resolveNestedElanousOption(opts);
|
|
774
|
+
const flags: string[] = [];
|
|
775
|
+
const values: Array<[string, string | undefined]> = [
|
|
776
|
+
['--base', opts.base], ['--target', opts.target], ['--correlation', opts.correlation],
|
|
777
|
+
['--goal-type', opts.goalType], ...(dryRunPreview.entrance === 'cli-harness-say' ? [['--author-grade', opts.authorGradeSource === 'flag' ? opts.authorGrade : undefined] as [string, string | undefined]] : []), ['--child-llm-provider', opts.childLlmProvider],
|
|
778
|
+
['--child-llm-model', opts.childLlmModel], ['--child-llm-effort', opts.childLlmEffort],
|
|
779
|
+
['--pod-pool', (opts as HarnessSubstrateOpts).podPool], ['--pod-memory', (opts as HarnessSubstrateOpts).podMemory],
|
|
780
|
+
['--after', (opts as HarnessSubstrateOpts).after], ['--source', (opts as HarnessSubstrateOpts).source],
|
|
781
|
+
];
|
|
782
|
+
for (const [flag, value] of values) if (value !== undefined) flags.push(flag, value);
|
|
783
|
+
if ((opts as HarnessSubstrateOpts).source !== undefined && resolved.substrate !== 'pod') {
|
|
784
|
+
console.error('`--source` 는 `--substrate pod` 와 함께');
|
|
785
|
+
process.exitCode = 2;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (resolved.substrate === 'pod' && opts.target !== undefined) {
|
|
789
|
+
console.error('`--target` 은 Pod 경로에서 아직 지원하지 않는다 — 로컬로 돌리거나 `--target` 을 빼라');
|
|
790
|
+
console.error('Pod 로 특정 원천을 주려면 `--source`');
|
|
791
|
+
process.exitCode = 2;
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
for (const [enabled, flag] of [
|
|
795
|
+
[opts.json, '--json'], [opts.yes, '--yes'], [opts.autoMerge === false, '--no-auto-merge'],
|
|
796
|
+
[opts.mergeByHost, '--merge-by-host'], [opts.observeOnly, '--observe-only'],
|
|
797
|
+
[opts.supervise === false, '--no-supervise'], [opts.forcePreflight, '--force-preflight'],
|
|
798
|
+
[opts.forceGate, '--force-gate'],
|
|
799
|
+
] as const) if (enabled) flags.push(flag);
|
|
800
|
+
let row: QueueItem;
|
|
801
|
+
try {
|
|
802
|
+
row = await addHarnessQueue({ seat: stampedSeat, launchCwd: process.cwd(), refuseDuplicate: true,
|
|
803
|
+
...(dryRunPreview.goalPath ? { ask: dryRunPreview.goalPath } : { say: dryRunPreview.input }),
|
|
804
|
+
launchArgs: ['harness', dryRunPreview.goalPath ? 'ask' : 'say', dryRunPreview.goalPath ?? dryRunPreview.input,
|
|
805
|
+
'--seat', stampedSeat, ...((opts as HarnessSubstrateOpts).substrate ? ['--substrate', (opts as HarnessSubstrateOpts).substrate!] : []), ...flags,
|
|
806
|
+
...(opts.nestedElanous ? ['--nested-elanous', opts.nestedElanous] : [])],
|
|
807
|
+
}, queueDeps);
|
|
808
|
+
await queueDeps?.afterEnqueue?.(row);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
if (!(error instanceof HarnessQueueDuplicateError)) throw error;
|
|
811
|
+
console.error(`발사 거절 — ${error.message.replace(/[\r\n]+/g, ' ')}`);
|
|
812
|
+
process.exitCode = 1;
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const result = await tickHarnessQueue({ ...queueDeps, ...(immediateLaunch ? { launch: immediateLaunch } : {}) }, row.id);
|
|
816
|
+
if (result.outcome === 'launched' && result.item?.id === row.id) {
|
|
817
|
+
if (immediateExit) process.exitCode = await immediateExit();
|
|
818
|
+
} else if (result.outcome === 'waiting') {
|
|
819
|
+
const reason = listHarnessQueue(queueDeps).find((item) => item.id === row.id)?.waitingReason ?? result.reason;
|
|
820
|
+
console.log(`대기열에 들어갔다 — ${reason.replace(/[\r\n]+/g, ' ')}`);
|
|
821
|
+
process.exitCode = 0;
|
|
822
|
+
} else if (result.outcome === 'skipped' && result.item?.id === row.id && result.item.status !== 'queued') {
|
|
823
|
+
// Another tick won the race for this row. Only a confirmed launch (pid recorded) is reported as launched;
|
|
824
|
+
// a row still `launching` may yet fail, so the direct call ends non-zero instead of claiming success.
|
|
825
|
+
const confirmed = (result.item.status === 'launched' || result.item.status === 'finished') && result.item.pid !== undefined;
|
|
826
|
+
const log = join(queueDeps?.root ?? effectiveInstanceRoot(), 'harness', `${row.id}.log`);
|
|
827
|
+
if (confirmed) console.log(`다른 대기열 tick 이 이미 발사했다 — ${row.id} (pid ${result.item.pid}) · 출력: ${log}`);
|
|
828
|
+
else console.error(`다른 대기열 tick 이 발사 중이다(확정 전) — ${row.id} (${result.item.status}) · harness queue list 로 확인`);
|
|
829
|
+
try { debug.log('harness.queue', 'raced-launch', { id: row.id, seat: row.seat, status: result.item.status, confirmed }); }
|
|
830
|
+
catch { /* Observation cannot change the launch outcome. */ }
|
|
831
|
+
process.exitCode = confirmed ? 0 : 1;
|
|
832
|
+
} else {
|
|
833
|
+
throw new Error(`harness queue: ${result.reason}`);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
735
837
|
// Depth >= 1 cannot re-allow. Drop the flag here so the launch sees the refusal, not the raw argv.
|
|
736
838
|
const nested = resolveNestedElanousOption(opts);
|
|
737
839
|
const stampedOpts = {
|
|
@@ -2413,6 +2515,30 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
|
|
|
2413
2515
|
installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
|
|
2414
2516
|
const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
|
|
2415
2517
|
const queueDeps = deps.queue ?? {};
|
|
2518
|
+
let launchedExit: Promise<number> | undefined;
|
|
2519
|
+
const immediateLaunch: NonNullable<HarnessQueueDeps['launch']> = queueDeps.launch ?? (async (item: QueueItem, args: string[], root: string) => {
|
|
2520
|
+
const child = spawn(process.execPath, [join(import.meta.dir, 'harness-queue-child.ts'),
|
|
2521
|
+
harnessQueueReceiptPath(root, item.launchId!), queueDeps.launchCommand ?? join(import.meta.dir, '../../bin/elanous.mjs'),
|
|
2522
|
+
...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
|
|
2523
|
+
cwd: item.launchCwd ?? resolve(import.meta.dir, '../..'), stdio: 'inherit',
|
|
2524
|
+
env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
|
|
2525
|
+
ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
|
|
2526
|
+
});
|
|
2527
|
+
// Capture the end at spawn time: a child that already closed (or died by a signal) must not leave the parent waiting.
|
|
2528
|
+
launchedExit = new Promise<number>((done) => child.once('close', (code, signal) => {
|
|
2529
|
+
const signo = signal ? (osConstants.signals as Record<string, number>)[signal] : undefined;
|
|
2530
|
+
done(code ?? (signo ? 128 + signo : 1));
|
|
2531
|
+
}));
|
|
2532
|
+
return await new Promise<number>((done, reject) => {
|
|
2533
|
+
child.once('spawn', () => done(child.pid!));
|
|
2534
|
+
child.once('error', (error) => {
|
|
2535
|
+
try { writeHarnessQueueReceipt(harnessQueueReceiptPath(root, item.launchId!), 'not-started'); }
|
|
2536
|
+
catch { /* An unreadable receipt leaves the reservation indeterminate. */ }
|
|
2537
|
+
reject(error);
|
|
2538
|
+
});
|
|
2539
|
+
});
|
|
2540
|
+
});
|
|
2541
|
+
const immediateExit = async (): Promise<number> => launchedExit ? await launchedExit : 0;
|
|
2416
2542
|
const queueAction = (action: () => Promise<void>): Promise<void> => runInjectedHarnessHandler(action);
|
|
2417
2543
|
queue.command('add').requiredOption('--seat <seat>', 'OP|TC|MK|UX')
|
|
2418
2544
|
.option('--say <sentence>', '원문 문장').option('--ask <goal-path>', '골 문서 경로')
|
|
@@ -2458,7 +2584,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
|
|
|
2458
2584
|
}
|
|
2459
2585
|
return resolved.substrate === 'pod' ? onPod(stampedOpts, 'cli-harness-ask', goalPath, resolved.pool!, deps.podDispatchTask) : (announceLocalChildProvider(stampedOpts), ask(goalPath, normalizeHarnessAskSayOptions(stampedOpts)));
|
|
2460
2586
|
},
|
|
2461
|
-
deps.launchGate,
|
|
2587
|
+
deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit,
|
|
2462
2588
|
);
|
|
2463
2589
|
});
|
|
2464
2590
|
}
|
|
@@ -2466,9 +2592,15 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
|
|
|
2466
2592
|
const say = deps.say;
|
|
2467
2593
|
if (say) {
|
|
2468
2594
|
registerHarnessAskSayOptions(harnessCmd.command('say <sentence...>').description('문장을 받아 구동한다'))
|
|
2595
|
+
.addOption(new Option('--author-grade <grade>', 'say 저작 등급 (full|lite)').choices(['full', 'lite']))
|
|
2469
2596
|
.action(async (sentence: string[], opts: HarnessAskSayChildLlmOptions & HarnessDryRunOpts) => {
|
|
2597
|
+
const grade = resolveGoalAuthorGrade(opts.authorGrade, getUserConfig().harness?.authorGrade);
|
|
2598
|
+
const podSource = process.env.ELANOUS_POD_AUTHOR_ON_POD === '1' && process.env.ELANOUS_POD_AUTHOR_GRADE === grade.grade
|
|
2599
|
+
? process.env.ELANOUS_POD_AUTHOR_GRADE_SOURCE : undefined;
|
|
2600
|
+
const gradedOpts = grade.source === 'default' && podSource === undefined ? opts
|
|
2601
|
+
: { ...opts, authorGrade: grade.grade, authorGradeSource: podSource === 'config' || podSource === 'default' || podSource === 'flag' ? podSource : grade.source };
|
|
2470
2602
|
await dispatchHarnessAskSay(
|
|
2471
|
-
|
|
2603
|
+
gradedOpts,
|
|
2472
2604
|
{
|
|
2473
2605
|
input: sentence.join(' '),
|
|
2474
2606
|
entrance: 'cli-harness-say',
|
|
@@ -2485,7 +2617,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
|
|
|
2485
2617
|
? onPod(stampedOpts, 'cli-harness-say', sentence.join(' '), resolved.pool!, deps.podDispatchTask, harnessAuthorOnPod())
|
|
2486
2618
|
: (announceLocalChildProvider(stampedOpts), say(sentence, normalizeHarnessAskSayOptions(stampedOpts)));
|
|
2487
2619
|
},
|
|
2488
|
-
deps.launchGate,
|
|
2620
|
+
deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit,
|
|
2489
2621
|
);
|
|
2490
2622
|
});
|
|
2491
2623
|
}
|
|
@@ -49,6 +49,8 @@ export interface HarnessPodDispatchInput {
|
|
|
49
49
|
readonly childLlmEffort?: string;
|
|
50
50
|
/** `harness.authorOnPod` — say 문장을 Pod 안에서 저작부터 돌린다. 호스트는 영수증만 남긴다. */
|
|
51
51
|
readonly authorOnPod?: boolean;
|
|
52
|
+
readonly authorGrade?: import('../self-implement/goal-author.js').GoalAuthorGrade;
|
|
53
|
+
readonly authorGradeSource?: import('../self-implement/goal-author.js').GoalAuthorGradeSource;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export function podOrchestrateArgs(input: HarnessPodDispatchInput, goalFile: string): string[] {
|
|
@@ -146,6 +148,15 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
|
|
|
146
148
|
if (input.after) env.ELANOUS_POD_AFTER = input.after;
|
|
147
149
|
if (input.authorOnPod) env.ELANOUS_POD_AUTHOR_ON_POD = '1';
|
|
148
150
|
else delete env.ELANOUS_POD_AUTHOR_ON_POD;
|
|
151
|
+
if (input.authorOnPod && input.authorGrade) {
|
|
152
|
+
env.ELANOUS_POD_AUTHOR_GRADE = input.authorGrade;
|
|
153
|
+
// 출처는 호스트가 알 때만 싣는다 — 모르면 «flag» 로 지어내지 않고 Pod 가 자기가 본 출처를 쓰게 둔다.
|
|
154
|
+
if (input.authorGradeSource) env.ELANOUS_POD_AUTHOR_GRADE_SOURCE = input.authorGradeSource;
|
|
155
|
+
else delete env.ELANOUS_POD_AUTHOR_GRADE_SOURCE;
|
|
156
|
+
} else {
|
|
157
|
+
delete env.ELANOUS_POD_AUTHOR_GRADE;
|
|
158
|
+
delete env.ELANOUS_POD_AUTHOR_GRADE_SOURCE;
|
|
159
|
+
}
|
|
149
160
|
if (input.entrance === 'cli-harness-ask') {
|
|
150
161
|
const root = findGitDir(cwd)?.root;
|
|
151
162
|
const path = resolve(cwd, input.input);
|
|
@@ -22,6 +22,7 @@ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
|
|
|
22
22
|
export type QueueItem = {
|
|
23
23
|
id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
|
|
24
24
|
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
|
|
25
|
+
launchArgs?: string[]; launchCwd?: string; cellId?: string;
|
|
25
26
|
};
|
|
26
27
|
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
27
28
|
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
@@ -53,6 +54,10 @@ export interface HarnessQueueDeps {
|
|
|
53
54
|
cap?: (seat: QueueSeat) => number;
|
|
54
55
|
pool?: () => QueuePool;
|
|
55
56
|
launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
|
|
57
|
+
/** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
|
|
58
|
+
afterEnqueue?: (item: QueueItem) => void | Promise<void>;
|
|
59
|
+
/** Script the default launcher runs under the queue child wrapper (default bin/elanous.mjs). */
|
|
60
|
+
launchCommand?: string;
|
|
56
61
|
alive?: (pid: number) => boolean;
|
|
57
62
|
processes?: () => readonly QueueProcess[];
|
|
58
63
|
receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
|
|
@@ -143,6 +148,9 @@ function read(path: string): QueueItem[] {
|
|
|
143
148
|
&& (row.pid === undefined || Number.isSafeInteger(row.pid))
|
|
144
149
|
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
145
150
|
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
151
|
+
&& (row.launchArgs === undefined || Array.isArray(row.launchArgs) && row.launchArgs.every((arg: unknown) => typeof arg === 'string'))
|
|
152
|
+
&& (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
|
|
153
|
+
&& (row.cellId === undefined || typeof row.cellId === 'string')
|
|
146
154
|
&& (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
|
|
147
155
|
throw new Error('harness queue: invalid queue file (no launch)');
|
|
148
156
|
}
|
|
@@ -207,7 +215,24 @@ function seat(value: string): QueueSeat {
|
|
|
207
215
|
return value as QueueSeat;
|
|
208
216
|
}
|
|
209
217
|
|
|
210
|
-
|
|
218
|
+
/** Cell id named in a goal text (`칸: ONEDOOR-2` · `칸 ONEDOOR-2`) — ids carry at least one hyphen. */
|
|
219
|
+
export function queueCellId(text: string): string | undefined {
|
|
220
|
+
return /칸[:\s]\s*([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/u.exec(text)?.[1];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A say row names its cell in the sentence; an ask row names it inside the goal document (unreadable → none). */
|
|
224
|
+
function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefined {
|
|
225
|
+
if (kind === 'say') return queueCellId(input);
|
|
226
|
+
try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class HarnessQueueDuplicateError extends Error {
|
|
230
|
+
constructor(existing: QueueItem, key: string) {
|
|
231
|
+
super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
|
|
211
236
|
const assigned = seat(input.seat);
|
|
212
237
|
if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
|
|
213
238
|
const kind = input.ask === undefined ? 'say' : 'ask';
|
|
@@ -228,8 +253,24 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
|
|
|
228
253
|
return existing;
|
|
229
254
|
}
|
|
230
255
|
}
|
|
256
|
+
// Recorded at enqueue: a later edit or deletion of an ask document cannot reopen its cell.
|
|
257
|
+
const cellId = queueRowCellId(kind, value);
|
|
258
|
+
if (input.refuseDuplicate) {
|
|
259
|
+
const cell = cellId;
|
|
260
|
+
const existing = rows.find((row) => row.status !== 'finished'
|
|
261
|
+
&& !['retryable', 'succeeded'].includes(queueRowOutcome(row, root))
|
|
262
|
+
&& ((row.kind === kind && row.input === value) || (cell !== undefined && (row.cellId ?? queueRowCellId(row.kind, row.input)) === cell)));
|
|
263
|
+
if (existing) {
|
|
264
|
+
const key = existing.kind === kind && existing.input === value ? '골' : `칸 ${cell}`;
|
|
265
|
+
observe('skipped', { id: existing.id, seat: assigned, reason: 'duplicate', key }, deps);
|
|
266
|
+
throw new HarnessQueueDuplicateError(existing, key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
231
269
|
const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
|
|
232
270
|
hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
|
|
271
|
+
...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
|
|
272
|
+
...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
|
|
273
|
+
...(cellId === undefined ? {} : { cellId }),
|
|
233
274
|
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
|
|
234
275
|
save(path, [...rows, item]);
|
|
235
276
|
observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
|
|
@@ -318,6 +359,7 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
|
|
|
318
359
|
}
|
|
319
360
|
|
|
320
361
|
export function queueLaunchArgs(item: QueueItem): string[] {
|
|
362
|
+
if (item.launchArgs) return item.launchArgs;
|
|
321
363
|
return ['harness', item.kind, item.input, '--substrate', 'pod',
|
|
322
364
|
...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
|
|
323
365
|
}
|
|
@@ -468,15 +510,19 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
|
|
|
468
510
|
}
|
|
469
511
|
}
|
|
470
512
|
|
|
471
|
-
|
|
513
|
+
function defaultLaunch(command?: string) {
|
|
514
|
+
return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function launch(item: QueueItem, args: string[], root: string, command?: string): Promise<number> {
|
|
472
518
|
const repo = resolve(import.meta.dir, '../..');
|
|
473
519
|
const logPath = join(root, 'harness', `${item.id}.log`);
|
|
474
520
|
const fd = openSync(logPath, 'a', 0o600);
|
|
475
521
|
try {
|
|
476
522
|
const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
|
|
477
|
-
harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
|
|
523
|
+
harnessQueueReceiptPath(root, item.launchId!), command ?? join(repo, 'bin', 'elanous.mjs'),
|
|
478
524
|
...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
|
|
479
|
-
cwd: repo, detached: true, stdio: ['ignore', fd, fd],
|
|
525
|
+
cwd: item.launchCwd ?? repo, detached: true, stdio: ['ignore', fd, fd],
|
|
480
526
|
env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
|
|
481
527
|
ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
|
|
482
528
|
});
|
|
@@ -675,7 +721,7 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
|
|
|
675
721
|
}
|
|
676
722
|
}
|
|
677
723
|
|
|
678
|
-
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
|
|
724
|
+
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
|
|
679
725
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
680
726
|
return locked(root, async (path) => {
|
|
681
727
|
try {
|
|
@@ -699,10 +745,20 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
699
745
|
}
|
|
700
746
|
const heads = new Map<QueueSeat, QueueItem>();
|
|
701
747
|
for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
|
|
748
|
+
if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
|
|
749
|
+
const row = items.find((item) => item.id === requestedId);
|
|
750
|
+
if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
|
|
751
|
+
const reason = '앞선 대기열 항목 차례';
|
|
752
|
+
if (row.waitingReason !== reason) save(path, items.map((item) => item.id === requestedId ? { ...item, waitingReason: reason } : item));
|
|
753
|
+
observe('waiting', { id: row.id, seat: row.seat, reason }, deps);
|
|
754
|
+
return { outcome: 'waiting', item: { ...row, waitingReason: reason }, reason };
|
|
755
|
+
}
|
|
702
756
|
if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
|
|
703
757
|
const lastSeat = lastLaunchedSeat(path);
|
|
704
758
|
const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
|
|
705
759
|
: queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
|
|
760
|
+
// A requested tick (direct CLI call) keeps the seat round-robin: it launches its row only when that row is the first
|
|
761
|
+
// head with room; an earlier seat's head with room means the requested row waits for its turn.
|
|
706
762
|
const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
|
|
707
763
|
.flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
|
|
708
764
|
const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
|
|
@@ -729,7 +785,9 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
729
785
|
...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
|
|
730
786
|
attributed, unattributed }, deps);
|
|
731
787
|
}
|
|
732
|
-
|
|
788
|
+
const subject = (requestedId ? candidates.find((item) => item.id === requestedId) : undefined) ?? candidates[0]!;
|
|
789
|
+
const subjectReason = blocked.get(subject.seat)!.reason;
|
|
790
|
+
return { outcome: 'waiting', item: { ...subject, waitingReason: subjectReason }, reason: requestedId ? subjectReason : reason };
|
|
733
791
|
};
|
|
734
792
|
for (const item of candidates) {
|
|
735
793
|
const injectedCap = deps.cap?.(item.seat);
|
|
@@ -799,11 +857,32 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
799
857
|
+ current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
|
|
800
858
|
}
|
|
801
859
|
if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
|
|
860
|
+
if (requestedId && item.id !== requestedId) {
|
|
861
|
+
const requested = current.find((row) => row.id === requestedId)!;
|
|
862
|
+
// The requested row's own block is the truthful reason — even when its seat comes later in the rotation and was not
|
|
863
|
+
// evaluated yet; «another seat's turn» only when the requested seat itself has room.
|
|
864
|
+
const own = blocked.get(requested.seat)?.reason ?? (() => {
|
|
865
|
+
const ownCap = deps.cap?.(requested.seat);
|
|
866
|
+
const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
|
|
867
|
+
const charged = runs.filter((run) => !run.seat || run.seat === requested.seat);
|
|
868
|
+
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
869
|
+
const active = charged.length + current.filter((row) => row.seat === requested.seat &&
|
|
870
|
+
(row.status === 'launching' || row.status === 'launched')
|
|
871
|
+
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
872
|
+
if (active >= details.cap) return seatCapReason(requested.seat, active, details);
|
|
873
|
+
const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
|
|
874
|
+
return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
|
|
875
|
+
})();
|
|
876
|
+
const reason = own ?? `다른 자리 차례 — ${item.seat}`;
|
|
877
|
+
if (requested.waitingReason !== reason) save(path, current.map((row) => row.id === requestedId ? { ...row, waitingReason: reason } : row));
|
|
878
|
+
observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
|
|
879
|
+
return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
|
|
880
|
+
}
|
|
802
881
|
const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
|
|
803
882
|
delete launching.waitingReason;
|
|
804
883
|
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
805
884
|
try {
|
|
806
|
-
const pid = await (deps.launch ??
|
|
885
|
+
const pid = await (deps.launch ?? defaultLaunch(deps.launchCommand))(launching, queueLaunchArgs(launching), root);
|
|
807
886
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
|
|
808
887
|
const launched: QueueItem = { ...launching, pid, status: 'launched' };
|
|
809
888
|
save(path, current.map((row) => row.id === item.id ? launched : row));
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// an allow flag is ignored and the launch stays refused.
|
|
14
14
|
|
|
15
15
|
import { debug } from '../debug/log.js';
|
|
16
|
+
import { getUserConfig } from '../user-config.js';
|
|
16
17
|
|
|
17
18
|
export const ELANOUS_NESTED_DEPTH_ENV = 'ELANOUS_NESTED_DEPTH';
|
|
18
19
|
|
|
@@ -26,7 +27,7 @@ export interface NestedElanousDecision {
|
|
|
26
27
|
readonly depth: number;
|
|
27
28
|
/** True when depth >= 1 supplied allow and it was ignored. */
|
|
28
29
|
readonly allowIgnored: boolean;
|
|
29
|
-
readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored';
|
|
30
|
+
readonly reason: 'depth-0' | 'depth-0-allow' | 'depth-unknown' | 'nested-refused' | 'allow-ignored' | 'depth-cap';
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/** Current process depth from the nested-depth marker. Unset or unreadable = 0 (outer launcher). */
|
|
@@ -56,11 +57,25 @@ export function nestedDepthCodexArgs(backendName: string, childDepth: number): s
|
|
|
56
57
|
* Allow decision. Depth 0 allows only when `allow` is true.
|
|
57
58
|
* Depth >= 1 always refuses; a supplied allow is ignored and logged.
|
|
58
59
|
*/
|
|
60
|
+
/** Nested depth cap: explicit value, else `harness.nestedElanousMaxDepth`, else 2. */
|
|
61
|
+
export function resolveNestedDepthCap(maxDepth?: number): number {
|
|
62
|
+
const configured = maxDepth ?? getUserConfig().harness?.nestedElanousMaxDepth;
|
|
63
|
+
return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0 ? configured : 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
export function decideNestedElanousLaunch(input: {
|
|
60
67
|
readonly depth: number;
|
|
61
68
|
readonly allow?: boolean;
|
|
69
|
+
readonly maxDepth?: number;
|
|
70
|
+
/** False for an ordinary mission spawn that did not request a nested launch — no cap log. */
|
|
71
|
+
readonly logCap?: boolean;
|
|
62
72
|
}): NestedElanousDecision {
|
|
63
73
|
const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
|
|
74
|
+
const cap = resolveNestedDepthCap(input.maxDepth);
|
|
75
|
+
if (depth >= cap) {
|
|
76
|
+
if (input.logCap !== false) debug.log('agent-mission.nested', 'depth-cap', { depth, cap });
|
|
77
|
+
return { allowed: false, depth, allowIgnored: input.allow === true, reason: 'depth-cap' };
|
|
78
|
+
}
|
|
64
79
|
if (depth >= 1) {
|
|
65
80
|
if (input.allow === true) {
|
|
66
81
|
debug.log('agent-mission.nested', 'allow-ignored', { depth });
|
|
@@ -101,14 +116,18 @@ export function gateNestedElanousLaunch(input: {
|
|
|
101
116
|
readonly nestedLaunchRequested: boolean;
|
|
102
117
|
readonly allow?: boolean;
|
|
103
118
|
readonly configAllow?: boolean;
|
|
119
|
+
readonly maxDepth?: number;
|
|
104
120
|
}): NestedElanousDecision {
|
|
105
121
|
const depth = Number.isInteger(input.depth) && input.depth >= 0 ? input.depth : 0;
|
|
122
|
+
const allow = input.nestedLaunchRequested && (input.allow === true || (depth === 0 && input.configAllow === true));
|
|
106
123
|
if (!input.nestedLaunchRequested) {
|
|
107
|
-
return decideNestedElanousLaunch({ depth, allow: false });
|
|
124
|
+
return decideNestedElanousLaunch({ depth, allow: false, logCap: false, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
|
|
108
125
|
}
|
|
109
126
|
if (!backendCarriesNestedDepth(input.backendName)) {
|
|
127
|
+
// The depth-unknown refusal stands; reaching the cap is still recorded.
|
|
128
|
+
const cap = resolveNestedDepthCap(input.maxDepth);
|
|
129
|
+
if (depth >= cap) debug.log('agent-mission.nested', 'depth-cap', { depth, cap, backend: input.backendName });
|
|
110
130
|
return { allowed: false, depth, allowIgnored: false, reason: 'depth-unknown' };
|
|
111
131
|
}
|
|
112
|
-
|
|
113
|
-
return decideNestedElanousLaunch({ depth, allow });
|
|
132
|
+
return decideNestedElanousLaunch({ depth, allow, ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }) });
|
|
114
133
|
}
|