elanous 0.2.15 → 0.2.16
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-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +36 -2
- package/src/harness/harness-pod-dispatch.ts +25 -55
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/hq/hq.ts +8 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +112 -6
- package/src/intake-plane/digest.ts +48 -26
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +90 -3
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/goal-author.ts +19 -13
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/orchestrator.ts +138 -7
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +36 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -33,7 +33,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
|
|
|
33
33
|
const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
|
|
34
34
|
imported_versions: ['json_hash', 'imported_at'],
|
|
35
35
|
events: ['reason'],
|
|
36
|
-
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date'],
|
|
36
|
+
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator'],
|
|
37
37
|
release_schedules: ['freeze_from', 'freeze_until'],
|
|
38
38
|
};
|
|
39
39
|
function schemaCurrent(db: Database): boolean {
|
|
@@ -75,7 +75,7 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
75
75
|
if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
|
|
76
76
|
db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
|
|
77
77
|
const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
|
|
78
|
-
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT']] as const) {
|
|
78
|
+
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER']] as const) {
|
|
79
79
|
if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
|
|
80
80
|
}
|
|
81
81
|
const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
|
|
@@ -169,7 +169,7 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
|
169
169
|
return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
|
|
170
170
|
}
|
|
171
171
|
function items(db: Database, version: string): ChecklistItem[] {
|
|
172
|
-
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
172
|
+
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
173
173
|
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | number | null>>;
|
|
174
174
|
return rows.map((r) => {
|
|
175
175
|
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
|
|
@@ -178,6 +178,7 @@ function items(db: Database, version: string): ChecklistItem[] {
|
|
|
178
178
|
...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors as string) as string[] } : {}),
|
|
179
179
|
...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
|
|
180
180
|
...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
|
|
181
|
+
...(r.accelerator === 1 ? { accelerator: true as const } : {}),
|
|
181
182
|
...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
|
|
182
183
|
updatedAt: r.updated_at as string, updatedBy: r.updated_by as string };
|
|
183
184
|
});
|
|
@@ -191,9 +192,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
|
|
|
191
192
|
if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
|
|
192
193
|
// features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
|
|
193
194
|
else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
|
|
194
|
-
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
195
|
-
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
196
|
-
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.updatedAt, item.updatedBy);
|
|
195
|
+
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
196
|
+
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
197
|
+
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
|
|
197
198
|
}
|
|
198
199
|
function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
|
|
199
200
|
|
|
@@ -234,8 +235,8 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
234
235
|
// Keep a legacy per-version title until an explicit retitle unifies the feature identity.
|
|
235
236
|
if (!existing) putItem(db, version, item);
|
|
236
237
|
else {
|
|
237
|
-
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
238
|
-
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.updatedAt, item.updatedBy);
|
|
238
|
+
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
239
|
+
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
for (const entry of data.history) record(db, version, entry);
|
|
@@ -262,7 +263,7 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
262
263
|
record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
|
|
263
264
|
continue;
|
|
264
265
|
}
|
|
265
|
-
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const;
|
|
266
|
+
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const;
|
|
266
267
|
const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
|
|
267
268
|
const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
|
|
268
269
|
const differs = fields.some((field) => field === 'evidence'
|
|
@@ -10,7 +10,7 @@ import { move, releasedVersion } from './feature-store.js';
|
|
|
10
10
|
import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
|
|
11
11
|
|
|
12
12
|
export type PlacementPriority = 'P0' | 'P1' | 'P2';
|
|
13
|
-
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }
|
|
13
|
+
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
|
|
14
14
|
export interface PlacementDecision { id: string; from: string | null; version: string; reason: string; displaced: Array<{ id: string; from: string; to: string; reason: string }> }
|
|
15
15
|
export interface RebalanceResult {
|
|
16
16
|
decisions: PlacementDecision[];
|
|
@@ -122,6 +122,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
122
122
|
validateCeoLoad(input);
|
|
123
123
|
const seat = parseOwner(input.owner).seat;
|
|
124
124
|
if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
125
|
+
if (input.accelerator !== undefined && input.accelerator !== true && input.accelerator !== false) throw new CliUserError('가속 등급은 true 이거나 생략한다');
|
|
125
126
|
const now = (deps.now ?? new Date()).getTime();
|
|
126
127
|
const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
127
128
|
const released = deps.released ?? releasedVersion();
|
|
@@ -142,6 +143,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
142
143
|
if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
|
|
143
144
|
if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
|
|
144
145
|
if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
|
|
146
|
+
const accelerator = input.accelerator === true || (input.accelerator === undefined && existing?.accelerator === true);
|
|
145
147
|
const load = { id: input.id, ceoMinutes: input.ceoMinutes ?? existing?.ceoMinutes, ceoDate: input.ceoDate ?? existing?.ceoDate };
|
|
146
148
|
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
147
149
|
const predecessors = input.predecessors.map((id) => {
|
|
@@ -170,6 +172,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
170
172
|
let chosen: ReleaseSchedule | undefined;
|
|
171
173
|
let loadIssue: string | null = null;
|
|
172
174
|
const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
|
|
175
|
+
// 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
|
|
173
176
|
for (const row of feasible) {
|
|
174
177
|
const placement = new Map([[input.id, row.version]]);
|
|
175
178
|
const items = occupied(row.version);
|
|
@@ -180,11 +183,15 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
180
183
|
chosen = row;
|
|
181
184
|
break;
|
|
182
185
|
}
|
|
183
|
-
|
|
186
|
+
const mayDisplace = input.priority === 'P1' ? from !== row.version : accelerator && from !== row.version;
|
|
187
|
+
if (!mayDisplace) { loadIssue ??= directIssue; continue; }
|
|
184
188
|
const next = open.find((later) => versionOrder(later.version, row.version) > 0);
|
|
185
189
|
if (!next || items.length - 1 >= capacity(row)) { loadIssue ??= directIssue; continue; }
|
|
190
|
+
const victimOk = (priority: PlacementPriority | undefined) => (input.priority === 'P1' && priority === 'P2')
|
|
191
|
+
|| (accelerator && priority === input.priority);
|
|
186
192
|
const p2 = [...items].reverse().find((item) => {
|
|
187
|
-
if (item.
|
|
193
|
+
if (item.accelerator === true) return false;
|
|
194
|
+
if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
|
|
188
195
|
if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
|
|
189
196
|
if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
|
|
190
197
|
if (!item.owner) return false;
|
|
@@ -202,22 +209,24 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
202
209
|
return true;
|
|
203
210
|
});
|
|
204
211
|
if (p2) {
|
|
205
|
-
displaced.push({ id: p2.id, from: row.version, to: next.version, reason:
|
|
212
|
+
displaced.push({ id: p2.id, from: row.version, to: next.version, reason: accelerator
|
|
213
|
+
? `가속 등급 ${input.id} 를 같은 우선순위 안에서 먼저 두기 위해 ${p2.priority ?? 'P2'} ${p2.id} 이월`
|
|
214
|
+
: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
|
|
206
215
|
chosen = row;
|
|
207
216
|
break;
|
|
208
217
|
}
|
|
209
218
|
loadIssue ??= directIssue;
|
|
210
219
|
}
|
|
211
220
|
if (!chosen) throw new CliUserError(loadIssue ?? `${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
|
|
212
|
-
const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
221
|
+
const reason = `${accelerator ? '가속 등급 · ' : ''}${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'}${accelerator ? ' · 같은 우선순위 안에서 가속 등급을 먼저' : ''} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
213
222
|
const decision = { id: input.id, from, version: chosen.version, reason, displaced };
|
|
214
223
|
if (!deps.dryRun) {
|
|
215
224
|
const by = deps.by ?? 'OP';
|
|
216
225
|
for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
|
|
217
226
|
if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
|
|
218
|
-
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate)))
|
|
219
|
-
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}) }, by);
|
|
220
|
-
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}) });
|
|
227
|
+
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate) || (input.accelerator !== undefined && (existing?.accelerator === true) !== (input.accelerator === true))))
|
|
228
|
+
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator !== undefined ? { accelerator: input.accelerator === true ? true : null } : {}) }, by);
|
|
229
|
+
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}), ...(accelerator ? { accelerator: true } : {}) });
|
|
221
230
|
}
|
|
222
231
|
return decision;
|
|
223
232
|
}
|
|
@@ -20,7 +20,7 @@ function createTable(store: MsgStore): void {
|
|
|
20
20
|
store.db.exec(`CREATE TABLE IF NOT EXISTS seat_asks (
|
|
21
21
|
id TEXT PRIMARY KEY, seat TEXT NOT NULL, origin TEXT NOT NULL,
|
|
22
22
|
deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT 120, status TEXT NOT NULL DEFAULT 'pending',
|
|
23
|
-
answer TEXT, answered_at INTEGER
|
|
23
|
+
answer TEXT, answered_at INTEGER, asked_at INTEGER
|
|
24
24
|
); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
|
|
25
25
|
id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
26
26
|
claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
|
|
@@ -28,6 +28,7 @@ function createTable(store: MsgStore): void {
|
|
|
28
28
|
const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
|
|
29
29
|
if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
|
|
30
30
|
if (!askColumns.some(({ name }) => name === 'answered_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answered_at INTEGER');
|
|
31
|
+
if (!askColumns.some(({ name }) => name === 'asked_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN asked_at INTEGER');
|
|
31
32
|
const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
|
|
32
33
|
if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
|
|
33
34
|
if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
|
|
@@ -87,8 +88,9 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
|
|
|
87
88
|
const store = (deps.open ?? openMsgStore)();
|
|
88
89
|
try {
|
|
89
90
|
createTable(store);
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const askedAt = (deps.now ?? Date.now)();
|
|
92
|
+
store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status, asked_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
93
|
+
.run(id, 'TC', JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
|
|
92
94
|
} finally { store.close(); }
|
|
93
95
|
try {
|
|
94
96
|
const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
|
|
@@ -119,12 +121,25 @@ export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps,
|
|
|
119
121
|
} finally { store.close(); }
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/** Return only the recorded reply's return address and measured ask time. */
|
|
125
|
+
export function seatAskReplyInfo(id: string, open: () => MsgStore = openMsgStore): { origin: AskOrigin; askedAt: number | null; answeredAt: number } {
|
|
126
|
+
const store = open();
|
|
127
|
+
try {
|
|
128
|
+
createTable(store);
|
|
129
|
+
const row = store.db.query('SELECT origin, asked_at, answered_at FROM seat_asks WHERE id = ? AND answer IS NOT NULL')
|
|
130
|
+
.get(id) as { origin: string; asked_at: number | null; answered_at: number } | null;
|
|
131
|
+
if (!row) throw new Error(`seat ask answer not recorded: ${id}`);
|
|
132
|
+
return { origin: JSON.parse(row.origin) as AskOrigin, askedAt: row.asked_at, answeredAt: row.answered_at };
|
|
133
|
+
} finally { store.close(); }
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
/** Deliver recorded seat answers or expiration notices via the original surface. */
|
|
123
|
-
export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
137
|
+
export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string }): Promise<void> {
|
|
124
138
|
const store = (deps.open ?? openMsgStore)();
|
|
125
139
|
try {
|
|
126
140
|
createTable(store);
|
|
127
|
-
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending'
|
|
141
|
+
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending' AND (? IS NULL OR id = ?)")
|
|
142
|
+
.all(deps.askId ?? null, deps.askId ?? null) as AskRow[];
|
|
128
143
|
const decide = store.db.transaction((id: string, now: number): 'answered' | 'expired' | null => {
|
|
129
144
|
const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
|
|
130
145
|
if (!ask) return null;
|
|
@@ -150,7 +165,8 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
150
165
|
const status = decide(ask.id, (deps.now ?? Date.now)());
|
|
151
166
|
if (status) debug.log('seat.ask', status, { seat: ask.seat, id: ask.id, via: origin.channel });
|
|
152
167
|
}
|
|
153
|
-
const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending'
|
|
168
|
+
const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending' AND (? IS NULL OR id = ?)")
|
|
169
|
+
.all(deps.askId ?? null, deps.askId ?? null) as Array<{ id: string; origin: string; text: string }>;
|
|
154
170
|
for (const item of outbox) {
|
|
155
171
|
const origin = JSON.parse(item.origin) as AskOrigin;
|
|
156
172
|
if (deps.channel && origin.channel !== deps.channel) continue;
|
|
@@ -177,6 +193,17 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
177
193
|
} finally { store.close(); }
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
/** A pending outbox with a live claim is being delivered by another worker. */
|
|
197
|
+
export function seatAskDeliveryInProgress(id: string, open: () => MsgStore = openMsgStore): boolean {
|
|
198
|
+
const store = open();
|
|
199
|
+
try {
|
|
200
|
+
createTable(store);
|
|
201
|
+
const row = store.db.query("SELECT claim_until FROM seat_ask_outbox WHERE id = ? AND status = 'pending' AND claim_token IS NOT NULL")
|
|
202
|
+
.get(id) as { claim_until: number } | null;
|
|
203
|
+
return row !== null && row.claim_until > Date.now();
|
|
204
|
+
} finally { store.close(); }
|
|
205
|
+
}
|
|
206
|
+
|
|
180
207
|
/** Confirm only this browser's displayed answers; polling alone never removes them. */
|
|
181
208
|
export function acknowledgeSeatAnswers(clientId: string, ids: string[], open: () => MsgStore = openMsgStore): void {
|
|
182
209
|
const store = open();
|
|
@@ -43,7 +43,7 @@ export type TcCandidate =
|
|
|
43
43
|
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
44
44
|
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
45
45
|
export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
|
|
46
|
-
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
46
|
+
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
47
47
|
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
|
|
48
48
|
|
|
49
49
|
export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
@@ -53,6 +53,8 @@ export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
|
53
53
|
}
|
|
54
54
|
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
|
|
55
55
|
export type SeatDeps = {
|
|
56
|
+
/** Version stamp for decision cards the seat loop raises. Default = the ledger's resolver (`git reflog` on the repo, up to 10 s) — tests inject a fixed one. */
|
|
57
|
+
resolveDecisionVersion?: (at: string) => { released: string | null; dev: string | null; codename: string | null };
|
|
56
58
|
personaConfig?: PersonaLoopConfig;
|
|
57
59
|
personaRegistry?: PersonaRegistry;
|
|
58
60
|
personaDir?: string;
|
|
@@ -256,6 +258,59 @@ function queueKey(seat: string, item: SeatItem): string {
|
|
|
256
258
|
return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
/** A decision ledger that raises cards — forwards the injected version resolver so tests never run the repo's `git reflog`. */
|
|
262
|
+
function cardLedger(root: string, deps: SeatDeps, now?: Date): DecisionLedger {
|
|
263
|
+
return new DecisionLedger({ stateDir: root, ...(now ? { now: () => now } : {}), ...(deps.resolveDecisionVersion ? { resolveVersion: deps.resolveDecisionVersion } : {}) });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DEFAULT_REPEAT_STOP = 3;
|
|
267
|
+
|
|
268
|
+
function repeatStopLimit(config: SeatLoopConfig): number {
|
|
269
|
+
const value = config.repeatStop;
|
|
270
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_REPEAT_STOP;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A finished attempt that never landed (no merged-PR evidence). `attempting` is the intent row of the same launch. A `refused` row whose reason is the AUTOQ retry note is the reconcile of a queued row, not a second try. */
|
|
274
|
+
function unlandedAttempt(entry: SeatEntry): boolean {
|
|
275
|
+
if (!entry.item || entry.action === 'seat-question' || entry.action === 'seat-answer' || entry.inquiry) return false;
|
|
276
|
+
if (entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'held' || entry.status === 'attempting') return false;
|
|
277
|
+
if (entry.status === 'refused' && entry.reason === 'AUTOQ failed or cancelled; eligible for retry') return false;
|
|
278
|
+
return entry.status === 'queued' || entry.status === 'refused' || entry.status === 'outcome-unknown';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function repeatStopReason(attempts: number): string {
|
|
282
|
+
return `같은 칸 ${attempts}회 착지 0 — 사람 판단`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function personReopened(ledger: readonly SeatEntry[], seat: string, item: SeatItem, root: string): boolean {
|
|
286
|
+
const key = handledItemKey(item);
|
|
287
|
+
const lastHold = [...ledger].reverse().find((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && entry.status === 'held');
|
|
288
|
+
if (!lastHold) return false;
|
|
289
|
+
const heldAt = Date.parse(lastHold.at || lastHold.ts || '');
|
|
290
|
+
if (!Number.isFinite(heldAt)) return false;
|
|
291
|
+
const ref = decisionRef(seat, item);
|
|
292
|
+
return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).some((card) => {
|
|
293
|
+
if (!card.refs?.includes(ref) || card.status !== 'decided' || !card.decidedAt) return false;
|
|
294
|
+
const decidedAt = Date.parse(card.decidedAt);
|
|
295
|
+
return Number.isFinite(decidedAt) && decidedAt > heldAt;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Prior attempts of this seat and handled item that finished without a landing. A queued row whose queue is still retryable has not finished. Queue rows with the same key count when the ledger has no receipt. */
|
|
300
|
+
function unlandedAttemptCount(seat: string, item: SeatItem, ledger: readonly SeatEntry[], root: string, deps: SeatDeps): number {
|
|
301
|
+
const key = handledItemKey(item);
|
|
302
|
+
const idem = queueKey(seat, item);
|
|
303
|
+
const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
|
|
304
|
+
const finished = (entry: SeatEntry) => unlandedAttempt(entry) && !(entry.status === 'queued' && entry.queueId && outcome(entry.queueId, root) === 'retryable');
|
|
305
|
+
const fromLedger = ledger.filter((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && finished(entry));
|
|
306
|
+
const seen = new Set(fromLedger.flatMap((entry) => entry.queueId ? [entry.queueId] : []));
|
|
307
|
+
let rows: ReturnType<typeof listHarnessQueue> = [];
|
|
308
|
+
try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
|
|
309
|
+
catch (error) { observe('repeat-stop-queue-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
|
|
310
|
+
const fromQueue = rows.filter((row) => row.seat === seat && row.idempotencyKey === idem && row.status === 'finished' && !seen.has(row.id));
|
|
311
|
+
return fromLedger.length + fromQueue.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
259
314
|
/** A crash (or a failed write) between AUTOQ enqueue and the `queued` row leaves an `attempting`/`outcome-unknown` row with no queue id. Find its queue row by the
|
|
260
315
|
* seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
|
|
261
316
|
function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
|
|
@@ -338,7 +393,7 @@ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: strin
|
|
|
338
393
|
|| (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
|
|
339
394
|
|| (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
|
|
340
395
|
|| (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
|
|
341
|
-
|| (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
|
|
396
|
+
|| (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown' || (entry.status === 'held' && !personReopened(ledger, entry.seat, entry.item!, root ?? ''))))
|
|
342
397
|
|| (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
|
|
343
398
|
later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
|
|
344
399
|
&& later.item.version === entry.item?.version && later.item.id === entry.item?.id
|
|
@@ -881,7 +936,7 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
|
|
|
881
936
|
if (judgment.state === 'absent') {
|
|
882
937
|
if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
|
|
883
938
|
const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
|
|
884
|
-
|
|
939
|
+
cardLedger(root, deps, now).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
|
|
885
940
|
scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
|
|
886
941
|
c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
|
|
887
942
|
q: '결측 이웃의 업무를 어떻게 처리할까?' },
|
|
@@ -1403,6 +1458,38 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1403
1458
|
return entry;
|
|
1404
1459
|
}
|
|
1405
1460
|
if (config.mode === 'live-safe') {
|
|
1461
|
+
const limit = repeatStopLimit(config);
|
|
1462
|
+
const attempts = unlandedAttemptCount(seat, item, ledger, stateRoot, deps);
|
|
1463
|
+
if (attempts >= limit && !personReopened(ledger, seat, item, stateRoot)) {
|
|
1464
|
+
const reason = repeatStopReason(attempts);
|
|
1465
|
+
entry.status = 'held';
|
|
1466
|
+
entry.reason = reason;
|
|
1467
|
+
const ref = decisionRef(seat, item);
|
|
1468
|
+
const prior = new DecisionLedger({ stateDir: stateRoot }).list({ status: 'all' }).find((card) => card.refs?.includes(ref) && card.status === 'open');
|
|
1469
|
+
if (!prior) {
|
|
1470
|
+
cardLedger(stateRoot, deps, now).raiseOnce({
|
|
1471
|
+
title: `${seat}: ${item.id} 반복 착지 0`,
|
|
1472
|
+
category: 'other',
|
|
1473
|
+
scqa: {
|
|
1474
|
+
s: `${seat} 칸 ${item.id} 가 착지 없이 ${attempts}회 끝났다.`,
|
|
1475
|
+
c: '같은 칸을 다시 발사하지 않고 사람 판단을 기다린다.',
|
|
1476
|
+
q: '이 칸을 계속할까, 멈출까?',
|
|
1477
|
+
},
|
|
1478
|
+
options: [
|
|
1479
|
+
{ key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
|
|
1480
|
+
{ key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
|
|
1481
|
+
],
|
|
1482
|
+
recommendation: { skipped: true, reason: '착지 없는 반복만으로 계속할지 자동 결정하지 않는다' },
|
|
1483
|
+
raisedBy: { agent: 'seat-loop' },
|
|
1484
|
+
refs: [ref],
|
|
1485
|
+
raisedAt: now.toISOString(),
|
|
1486
|
+
crossCheckSkipped: '반복 정지는 사람 판단',
|
|
1487
|
+
}, ref);
|
|
1488
|
+
}
|
|
1489
|
+
append(path, entry);
|
|
1490
|
+
observe('repeat-stopped', { seat, item: item.id, attempts, lastQueueId: [...ledger].reverse().find((row) => row.seat === seat && row.item && handledItemKey(row.item) === handledItemKey(item) && row.queueId)?.queueId ?? null });
|
|
1491
|
+
return entry;
|
|
1492
|
+
}
|
|
1406
1493
|
let queued: { id: string };
|
|
1407
1494
|
try {
|
|
1408
1495
|
queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// ⛔ 종료(`process.exit`)는 여기가 «안» 한다 — 호출자가 자기 표면에 맞게 끝낸다.
|
|
12
12
|
// 그래서 결과를 «세 값»으로 낸다: 발사 / 저작 전 중단 / 전제 검사 중단.
|
|
13
13
|
import { linesOutsideFencedCode } from '../self-implement/goal-author.js';
|
|
14
|
+
import { recordAuthoredGoal } from './authored-goal-trace.js';
|
|
14
15
|
import { retiredEntranceNotice, type EntranceDeclaration } from './entrance-registry.js';
|
|
15
16
|
import {
|
|
16
17
|
classifyConcurrentAuthoring,
|
|
@@ -58,7 +59,7 @@ function resolveSelfResolveClarifications(explicit: boolean | undefined): Resolv
|
|
|
58
59
|
/** 저작기 산출 — `runGoalAuthorCli` 의 반환에서 이 흐름이 «실제로 쓰는» 것만. */
|
|
59
60
|
export interface AskAuthoredGoal {
|
|
60
61
|
readonly path: string;
|
|
61
|
-
readonly authored?: { readonly document?: string; readonly grounded?: boolean | null } | null;
|
|
62
|
+
readonly authored?: { readonly document?: string; readonly grounded?: boolean | null; readonly authorRunId?: string } | null;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export type LaunchingTreeProbe =
|
|
@@ -158,6 +159,8 @@ export interface AskLaunchFlowDeps {
|
|
|
158
159
|
/** ⛔ 못 얻으면 `null` — 같은 이유. */
|
|
159
160
|
recentAuthoringSamples(): RecentAuthoringSample[] | null;
|
|
160
161
|
authorGoal(args: readonly string[], options: Record<string, unknown>): Promise<AskAuthoredGoal>;
|
|
162
|
+
/** AUTHOR-TRACE seam (tests). */
|
|
163
|
+
recordAuthoredGoal?: typeof recordAuthoredGoal;
|
|
161
164
|
/** Pod launches reserve a host slot before goal authoring begins. */
|
|
162
165
|
beforeAuthoring?(): Promise<void>;
|
|
163
166
|
/** 발사 전 권고용 분해 seam. 생략하면 전용 관측명을 준 기존 분해기를 쓴다. */
|
|
@@ -861,6 +864,24 @@ export async function runAskLaunchFlow(
|
|
|
861
864
|
selfResolveClarifications: selfResolveClarifications.value,
|
|
862
865
|
});
|
|
863
866
|
deps.print(`[ask] 저작됨: ${authored.path}`);
|
|
867
|
+
// AUTHOR-TRACE: the authored goal file can be cleaned up later — keep path ⊕ sha256 ⊕ size in the run ledger and
|
|
868
|
+
// archive the body, so authoring length is measurable after the fact (GOAL-BENCH · AUTHOR-LITE).
|
|
869
|
+
const traceIds = {
|
|
870
|
+
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
871
|
+
...(authored.authored?.authorRunId === undefined ? {} : { authorRunId: authored.authored.authorRunId }),
|
|
872
|
+
};
|
|
873
|
+
if (authored.authored?.document !== undefined && (traceIds.runId !== undefined || traceIds.authorRunId !== undefined)) {
|
|
874
|
+
try {
|
|
875
|
+
const trace = (deps.recordAuthoredGoal ?? recordAuthoredGoal)(traceIds, authored.path, authored.authored.document);
|
|
876
|
+
deps.log('goal-authored-trace', { ...traceIds, ...trace }, 'info');
|
|
877
|
+
} catch (error) {
|
|
878
|
+
deps.log('goal-authored-trace-failed', { ...traceIds, error: error instanceof Error ? error.message : String(error) }, 'warn');
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
deps.log('goal-authored-trace-skipped', {
|
|
882
|
+
reason: authored.authored?.document === undefined ? 'no-authored-document' : 'no-run-or-author-id', path: authored.path,
|
|
883
|
+
}, 'warn');
|
|
884
|
+
}
|
|
864
885
|
const markerCounts = authored.authored?.document === undefined
|
|
865
886
|
? null
|
|
866
887
|
: countMissingAuthoredConstraintMarkers(input.askText, authored.authored.document);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { appendRunLedgerEntry, runLedgerDir } from '../self-implement/run-ledger.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
|
|
7
|
+
/** AUTHOR-TRACE: what `harness say`/`ask` authored, kept with the run — the goal file itself may be cleaned up later,
|
|
8
|
+
* so the ledger carries path ⊕ sha256 ⊕ size and the body is archived next to the ledger. */
|
|
9
|
+
export interface AuthoredGoalTrace {
|
|
10
|
+
path: string;
|
|
11
|
+
sha256: string;
|
|
12
|
+
chars: number;
|
|
13
|
+
sections: number;
|
|
14
|
+
archivedAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `## ` headings outside fenced code (a fence closes only on the same marker, at least as long). */
|
|
18
|
+
function countSections(document: string): number {
|
|
19
|
+
let fence: { char: string; length: number } | null = null;
|
|
20
|
+
let count = 0;
|
|
21
|
+
for (const line of document.split('\n')) {
|
|
22
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
23
|
+
if (fence) {
|
|
24
|
+
if (marker && marker[1]![0] === fence.char && marker[1]!.length >= fence.length && line.trim() === marker[1]) fence = null;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (marker) { fence = { char: marker[1]![0]!, length: marker[1]!.length }; continue; }
|
|
28
|
+
if (/^ {0,3}##\s/.test(line)) count += 1;
|
|
29
|
+
}
|
|
30
|
+
return count;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function authoredGoalTrace(path: string, document: string, archivedAt: string): AuthoredGoalTrace {
|
|
34
|
+
return {
|
|
35
|
+
path,
|
|
36
|
+
sha256: createHash('sha256').update(document).digest('hex'),
|
|
37
|
+
chars: [...document].length,
|
|
38
|
+
sections: countSections(document),
|
|
39
|
+
archivedAt,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Archive the body, emit one `goal-author`/`goal-authored` observation and — when the run id is already known —
|
|
44
|
+
* append a `goal-authored` run-ledger event. At `harness say` authoring time the run id usually does not exist yet;
|
|
45
|
+
* the authoring run id (`authorRunId`, joined to the goal by `goal-id-assigned`) keys the record then. */
|
|
46
|
+
export function recordAuthoredGoal(
|
|
47
|
+
ids: { runId?: string; authorRunId?: string },
|
|
48
|
+
path: string,
|
|
49
|
+
document: string,
|
|
50
|
+
options: { ledgerDir?: string; archiveDir?: string } = {},
|
|
51
|
+
): AuthoredGoalTrace {
|
|
52
|
+
const key = ids.runId ?? ids.authorRunId;
|
|
53
|
+
if (!key) throw new Error('recordAuthoredGoal needs a runId or an authorRunId');
|
|
54
|
+
const ledgerDir = options.ledgerDir ?? runLedgerDir();
|
|
55
|
+
const archiveDir = options.archiveDir ?? join(dirname(ledgerDir), 'authored-goals');
|
|
56
|
+
const digest = createHash('sha256').update(document).digest('hex');
|
|
57
|
+
const archived = join(archiveDir, `${key}-${digest.slice(0, 12)}.md`);
|
|
58
|
+
if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
|
|
59
|
+
writeFileSync(archived, document, 'utf8');
|
|
60
|
+
const trace = authoredGoalTrace(path, document, archived);
|
|
61
|
+
debug.log('goal-author', 'goal-authored', { ...ids, ...trace });
|
|
62
|
+
if (ids.runId) appendRunLedgerEntry({ runId: ids.runId, event: 'goal-authored', data: { ...trace, ...(ids.authorRunId ? { authorRunId: ids.authorRunId } : {}) } }, ledgerDir);
|
|
63
|
+
return trace;
|
|
64
|
+
}
|
package/src/self-dev/dev-cli.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { observeDeliverables, type DeliverableObservationResult, type Deliverabl
|
|
|
43
43
|
import { debug } from '../debug/log.js';
|
|
44
44
|
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
45
45
|
import { resolveChildLlmPreference, type ChildLlmPreferenceInput, type ResolvedChildLlmPreference } from '../self-implement/child-llm-preference.js';
|
|
46
|
+
import { applyLaunchOverloadFailover, codexProviderNamed } from '../oauth/overload-failover-launch.js';
|
|
46
47
|
import { readCachedGrokQuota } from '../oauth/codex-account-store.js';
|
|
47
48
|
import { queryAbandonedDraftPrs, type AbandonedDraftPr } from '../cli/logs-abandoned-draft-prs.js';
|
|
48
49
|
import { branchLineageSlug } from '../cli/pr-lineage.js';
|
|
@@ -1631,6 +1632,18 @@ function resolveChildLlmFromParts(
|
|
|
1631
1632
|
return undefined;
|
|
1632
1633
|
}
|
|
1633
1634
|
|
|
1635
|
+
/** Pinned `tools.selfImplement.childLlm` naming codex. Fallback-chain codex is not pinned. */
|
|
1636
|
+
function childLlmCodexPinned(flagProvider: string | undefined): boolean {
|
|
1637
|
+
if (flagProvider !== undefined) return false;
|
|
1638
|
+
try {
|
|
1639
|
+
const preference = resolveChildLlmPreference(getUserConfig());
|
|
1640
|
+
const provider = preference.chain[0]?.provider;
|
|
1641
|
+
return preference.mode === 'pinned' && preference.source.chain === 'pinned' && codexProviderNamed(provider);
|
|
1642
|
+
} catch {
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1634
1647
|
export function buildChildLlmSelection(
|
|
1635
1648
|
opts: Pick<DevCliOpts, 'childLlmProvider' | 'childLlmModel' | 'childLlmEffort'> & {
|
|
1636
1649
|
/** 발사 인자가 있을 때 관측용 선호를 읽는다. 없으면 getUserConfig. 기존 두 인자 시그니처는 그대로다. */
|
|
@@ -1967,7 +1980,12 @@ export function buildDevCliSpec(
|
|
|
1967
1980
|
}
|
|
1968
1981
|
|
|
1969
1982
|
if (path === 'self-mission') {
|
|
1970
|
-
const
|
|
1983
|
+
const builtChild = buildChildLlmSelection({ ...opts, readPreference: () => getUserConfig() });
|
|
1984
|
+
const childLlm = applyLaunchOverloadFailover(builtChild, {
|
|
1985
|
+
provider: opts.childLlmProvider,
|
|
1986
|
+
model: opts.childLlmModel,
|
|
1987
|
+
codexPinned: childLlmCodexPinned(opts.childLlmProvider),
|
|
1988
|
+
});
|
|
1971
1989
|
// 잔량 조회(약 3초)는 한 발사에 한 번만 — 자식·부모가 같은 값을 쓴다.
|
|
1972
1990
|
let grokQuotaOnce: ReturnType<typeof readCachedGrokQuota> | undefined;
|
|
1973
1991
|
const readGrokQuotaOnce = () => (grokQuotaOnce ??= readGrokQuota());
|
|
@@ -1863,9 +1863,9 @@ async function observeDevTaskDispatch(spec: DevPipelineSpec, plan: ResolvedDevPl
|
|
|
1863
1863
|
}
|
|
1864
1864
|
}
|
|
1865
1865
|
|
|
1866
|
-
async function inspectReleasePathBeforeMerge(seams: SelfImplementSeams, number: number, cwd: string): Promise<string | undefined> {
|
|
1866
|
+
async function inspectReleasePathBeforeMerge(seams: SelfImplementSeams, number: number, cwd: string, base?: string): Promise<string | undefined> {
|
|
1867
1867
|
if (!seams.readPrFiles) throw new Error('release-path inspection unavailable — automatic merge refused');
|
|
1868
|
-
const files = await seams.readPrFiles({ number, cwd });
|
|
1868
|
+
const files = await seams.readPrFiles({ number, cwd, ...(base ? { base } : {}) });
|
|
1869
1869
|
if (!Array.isArray(files) || !files.every((path) => typeof path === 'string')) throw new Error('invalid PR file list — automatic merge refused');
|
|
1870
1870
|
const path = releasePathHold(files);
|
|
1871
1871
|
if (!path) return undefined;
|
|
@@ -2107,10 +2107,11 @@ async function runDevPipelineDispatch(
|
|
|
2107
2107
|
...assembledSeams,
|
|
2108
2108
|
...(assembledSeams.mergePr ? { mergePr: async (input) => {
|
|
2109
2109
|
let path: string | undefined;
|
|
2110
|
-
try { path = await inspectReleasePathBeforeMerge(assembledSeams, input.number, input.cwd); }
|
|
2110
|
+
try { path = await inspectReleasePathBeforeMerge(assembledSeams, input.number, input.cwd, plan.base); }
|
|
2111
2111
|
catch (error) {
|
|
2112
|
-
|
|
2113
|
-
|
|
2112
|
+
const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
|
|
2113
|
+
debug.log('self-dev.merge', 'release-path-inspection-failed', { number: input.number, error: String(error), ...(rateLimited ? { rateLimit: true } : {}) });
|
|
2114
|
+
return { merged: false, detail: rateLimited ? `automatic merge refused: rate-limit: ${String(error)}` : `automatic merge refused: ${String(error)}` };
|
|
2114
2115
|
}
|
|
2115
2116
|
if (path) return { merged: false, detail: `OP approval required: ${path}` };
|
|
2116
2117
|
return assembledSeams.mergePr!(input);
|