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
package/src/cli/self-update.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { decideRestartNeeded, type RestartNeededResult } from './nexus-restart-needed.js';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
7
|
import { envLiteral } from '../platform/env-literal.js';
|
|
8
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
9
|
+
import { getUserConfig } from '../user-config.js';
|
|
10
|
+
import { planEdgeRail, type EdgeRailInput, type EdgeRailPlan } from './self-update-canary.js';
|
|
11
|
+
import { loadRunLedger } from '../self-implement/run-ledger.js';
|
|
8
12
|
|
|
9
13
|
export interface SelfUpdateOptions {
|
|
10
14
|
from?: string;
|
|
@@ -22,6 +26,14 @@ export interface SelfUpdateOptions {
|
|
|
22
26
|
dev?: boolean;
|
|
23
27
|
/** install.json 의 이전 versionDir 로 current 를 되돌린다. 이전이 없으면 거부. */
|
|
24
28
|
rollback?: boolean;
|
|
29
|
+
/** Canary rollout action; legacy --auto on|off|status is handled by the CLI scheduler. */
|
|
30
|
+
auto?: boolean;
|
|
31
|
+
dryRun?: boolean;
|
|
32
|
+
seat?: string;
|
|
33
|
+
quietWindow?: boolean;
|
|
34
|
+
failureRate?: EdgeRailInput['failureRate'];
|
|
35
|
+
/** Unique ID of an independently successful canary run; no id means no new run. */
|
|
36
|
+
canaryRunId?: string;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export interface SelfUpdateDeps {
|
|
@@ -30,6 +42,8 @@ export interface SelfUpdateDeps {
|
|
|
30
42
|
decide?: (opts: { to: string; cwd: string; out: { log: (s: string) => void; error: (s: string) => void } }) => Promise<RestartNeededResult>;
|
|
31
43
|
run?: (command: string, args: string[], cwd: string, options?: { timeout?: number }) => { status: number | null; stderr: string; stdout?: string };
|
|
32
44
|
installedVersion?: () => string;
|
|
45
|
+
/** 캐너리 시작 «뒤»에 시작해 `completed` 로 끝난 런인가(주입 안 하면 런 원장 `canaryRunSucceededInLedger`). */
|
|
46
|
+
canaryRunSucceeded?: (runId: string, sinceIso: string) => boolean;
|
|
33
47
|
/** 재시작 뒤 데몬이 이 커밋으로 떴나(주입 안 하면 `/v1/health` 를 최대 90초 폴링). */
|
|
34
48
|
verifyRestart?: (expectedCommit: string) => Promise<VerifyRestartResult>;
|
|
35
49
|
/** 판 목록(이름) — 되돌릴 판 찾기. */
|
|
@@ -58,6 +72,13 @@ export interface SelfUpdateDeps {
|
|
|
58
72
|
packageVersion?: (checkout: string) => string;
|
|
59
73
|
/** 재시작을 실제로 호출해도 되는지. NODE_ENV=test · ELANOUS_TEST_HOME 이면 기본 false. */
|
|
60
74
|
allowLiveRestart?: boolean;
|
|
75
|
+
/** Automatic edge-rail inputs; injected in tests, never a service registration. */
|
|
76
|
+
now?: () => Date;
|
|
77
|
+
stateDir?: string;
|
|
78
|
+
edgeRailConfig?: () => Pick<EdgeRailInput, 'canaryOkRuns' | 'failureMultiplier' | 'minSamples'>;
|
|
79
|
+
installEdgeRail?: (options: SelfUpdateOptions, deps: SelfUpdateDeps) => Promise<SelfUpdateResult>;
|
|
80
|
+
rollbackEdgeRail?: (deps: Pick<SelfUpdateDeps, 'installRoot'>) => ReturnType<typeof rollbackDevInstall>;
|
|
81
|
+
restartEdgeRail?: () => Promise<boolean>;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
export interface SelfUpdateResult {
|
|
@@ -87,6 +108,10 @@ export interface SelfUpdateResult {
|
|
|
87
108
|
relay?: RelayUpdateOutcome;
|
|
88
109
|
/** 텔레그램 러너(`com.elanous.telegram` · `elanous-telegram.service`) 판정 — 🆕 2026-09-25. */
|
|
89
110
|
telegramRunner?: RelayUpdateOutcome;
|
|
111
|
+
edgeRail?: EdgeRailPlan;
|
|
112
|
+
missingFixes?: string;
|
|
113
|
+
/** 실패율 측정 원천이 없으면 자동 되돌림이 판단되지 않는다 — 측정 원천은 다음 조각(EDGE-RAIL2 범위 밖). */
|
|
114
|
+
autoRollback?: 'active' | 'inactive-no-failure-rate';
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
export interface RelayUpdateOutcome {
|
|
@@ -628,12 +653,186 @@ function readInstallSource(cliRoot: string, exists: (path: string) => boolean, r
|
|
|
628
653
|
}
|
|
629
654
|
}
|
|
630
655
|
|
|
656
|
+
/** Output only; never uses a title to decide whether to install. */
|
|
657
|
+
export function missingFixCommitTitles(checkout: string, installed: string, main: string, git: NonNullable<SelfUpdateDeps['git']>): string {
|
|
658
|
+
if (!/^[a-f0-9]{12,40}$/i.test(installed) || !/^[a-f0-9]{12,40}$/i.test(main)) return 'main 엔 있고 운영엔 없는 수리: 판정 불가';
|
|
659
|
+
const result = git(checkout, ['log', '--format=%s', '--no-merges', `${installed}..${main}`]);
|
|
660
|
+
if (result.status !== 0) return 'main 엔 있고 운영엔 없는 수리: 판정 불가';
|
|
661
|
+
const titles = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => /\bfix(?:es|ed)?\b|(?:^|[(:])fix[:)]/i.test(line))
|
|
662
|
+
.map((line) => line.replace(/[\r\n\t]+/g, ' ').slice(0, 160)).slice(0, 10);
|
|
663
|
+
return `main 엔 있고 운영엔 없는 수리: ${titles.length ? titles.join(' · ') : '없음'}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
interface EdgeRailState { canary: NonNullable<EdgeRailInput['canary']>; mainCommit: string; lastRunId?: string; countedRunIds?: string[]; failureRate?: NonNullable<EdgeRailInput['failureRate']>; promoted?: boolean; rolledBack?: boolean }
|
|
667
|
+
|
|
668
|
+
function saveEdgeRailState(file: string, state: EdgeRailState): void {
|
|
669
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
670
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
671
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
672
|
+
renameSync(tmp, file);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** 런 원장에서: `start` 가 캐너리 시작 이후이고 마지막 `run-status` 가 `completed` 인 런만 참. 못 읽으면 거짓. */
|
|
676
|
+
export function canaryRunSucceededInLedger(runId: string, sinceIso: string, dir?: string): boolean {
|
|
677
|
+
try {
|
|
678
|
+
const entries = (dir ? loadRunLedger(runId, dir) : loadRunLedger(runId)) ?? [];
|
|
679
|
+
const started = Date.parse(entries.find((entry) => entry.event === 'start')?.timestamp ?? '');
|
|
680
|
+
const last = entries.filter((entry) => entry.event === 'run-status').at(-1);
|
|
681
|
+
return Number.isFinite(started) && started >= Date.parse(sinceIso) && last?.data.runStatus === 'completed';
|
|
682
|
+
} catch { return false; }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function restartEdgeRail(deps: SelfUpdateDeps, checkout: string): Promise<boolean> {
|
|
686
|
+
if (deps.restartEdgeRail) return deps.restartEdgeRail();
|
|
687
|
+
if (deps.allowLiveRestart === false || (deps.allowLiveRestart !== true && liveRestartBlocked())) return false;
|
|
688
|
+
const os = deps.os ?? platform();
|
|
689
|
+
const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
|
|
690
|
+
if (!command) return false;
|
|
691
|
+
const args = os === 'darwin' ? ['kickstart', '-k', `gui/${deps.uid ?? process.getuid?.() ?? userInfo().uid}/com.elanous.nexus`] : ['--user', 'restart', 'elanous-nexus'];
|
|
692
|
+
return (deps.run ?? execute)(command, args, checkout).status === 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export async function runEdgeRailUpdate(options: SelfUpdateOptions, deps: SelfUpdateDeps = {}): Promise<SelfUpdateResult> {
|
|
696
|
+
const out = deps.out ?? console;
|
|
697
|
+
const root = deps.installRoot ?? defaultInstallRoot();
|
|
698
|
+
const record = readInstallRecord(root);
|
|
699
|
+
const checkout = resolve(options.from ?? record?.source ?? deps.cliRoot ?? resolve(import.meta.dir, '../..'));
|
|
700
|
+
const stateFile = join(deps.stateDir ?? elanousStateRoot(), 'self-update-edge-rail.json');
|
|
701
|
+
const git = deps.git ?? ((cwd: string, args: string[]) => execute('git', args, cwd));
|
|
702
|
+
const fail = (reason: string): SelfUpdateResult => {
|
|
703
|
+
const result: SelfUpdateResult = { exitCode: 2, installedVersion: null, decision: null, restarted: false, reason };
|
|
704
|
+
out.log(options.json ? JSON.stringify(result) : `self-update --auto: hold — ${reason}`);
|
|
705
|
+
return result;
|
|
706
|
+
};
|
|
707
|
+
if (options.dev || options.rollback || options.version || options.restart) return fail('--auto cannot combine with --dev, --rollback, --version or --restart');
|
|
708
|
+
if (options.seat !== 'OP' && options.seat !== 'TC' && options.seat !== 'MK' && options.seat !== 'UX') return fail('--auto requires a known --seat (OP|TC|MK|UX)');
|
|
709
|
+
if (!record?.commit) return fail('installed commit unknown');
|
|
710
|
+
const currentName = versionDirName(record.versionDir);
|
|
711
|
+
if (!currentName || !installEntryExists(root, currentName)) return fail('installed entry missing');
|
|
712
|
+
try {
|
|
713
|
+
if (readlinkSync(join(root, 'current')) !== record.versionDir) return fail('current does not match installed record');
|
|
714
|
+
} catch { return fail('current is not a version link'); }
|
|
715
|
+
if (!options.from && (!record.source || /^(?:https?:|file:)/.test(record.source))) return fail('local checkout source unknown — use --from');
|
|
716
|
+
if (!existsSync(join(checkout, 'scripts', 'install.sh'))) return fail('checkout installer missing');
|
|
717
|
+
const verifiedRoot = git(checkout, ['rev-parse', '--show-toplevel']);
|
|
718
|
+
try {
|
|
719
|
+
if (verifiedRoot.status !== 0 || !verifiedRoot.stdout.trim() || realpathSync(verifiedRoot.stdout.trim()) !== realpathSync(checkout)) return fail('checkout git root mismatch');
|
|
720
|
+
} catch { return fail('checkout git root mismatch'); }
|
|
721
|
+
const head = git(checkout, ['rev-parse', '--verify', 'main']);
|
|
722
|
+
if (head.status !== 0 || !/^[a-f0-9]{12,40}$/i.test(head.stdout.trim())) return fail('main commit unknown');
|
|
723
|
+
if (!options.dryRun) {
|
|
724
|
+
const tracked = git(checkout, ['diff', '--quiet', 'HEAD', '--']);
|
|
725
|
+
const working = git(checkout, ['rev-parse', '--verify', 'HEAD']);
|
|
726
|
+
if (tracked.status !== 0 || working.status !== 0 || working.stdout.trim() !== head.stdout.trim()) return fail('main is not the clean checkout HEAD');
|
|
727
|
+
const untracked = git(checkout, ['ls-files', '--others', '--exclude-standard']);
|
|
728
|
+
if (untracked.status !== 0 || untracked.stdout.trim()) return fail('checkout contains untracked files');
|
|
729
|
+
}
|
|
730
|
+
let state: EdgeRailState | null = null;
|
|
731
|
+
if (existsSync(stateFile)) {
|
|
732
|
+
try {
|
|
733
|
+
const value: unknown = JSON.parse(readFileSync(stateFile, 'utf8'));
|
|
734
|
+
if (value && typeof value === 'object' && 'canary' in value && 'mainCommit' in value) state = value as EdgeRailState;
|
|
735
|
+
else return fail('invalid edge-rail state');
|
|
736
|
+
} catch { return fail('invalid edge-rail state'); }
|
|
737
|
+
}
|
|
738
|
+
const mainCommit = head.stdout.trim();
|
|
739
|
+
const config = deps.edgeRailConfig?.() ?? getUserConfig().harness?.edgeRail ?? {};
|
|
740
|
+
const canary = state?.mainCommit === mainCommit ? state.canary : null;
|
|
741
|
+
const observed = (deps.now ?? (() => new Date()))();
|
|
742
|
+
if (!options.dryRun && canary && canary.okRuns < (config.canaryOkRuns ?? 3) && options.seat === 'OP' && (record.commit.startsWith(mainCommit) || mainCommit.startsWith(record.commit))
|
|
743
|
+
&& !options.failureRate && options.canaryRunId?.trim() && options.canaryRunId !== state?.lastRunId
|
|
744
|
+
// 같은 런을 두 번 세지 않고, 캐너리 시작 뒤 «성공으로 끝난» 런만 센다(임의·과거·실패 id 로 승격을 열지 못하게).
|
|
745
|
+
&& !(state?.countedRunIds ?? []).includes(options.canaryRunId.trim())
|
|
746
|
+
&& (deps.canaryRunSucceeded ?? canaryRunSucceededInLedger)(options.canaryRunId.trim(), canary.startedAt)
|
|
747
|
+
&& Number.isFinite(observed.getTime()) && observed.getTime() >= Date.parse(canary.startedAt)) {
|
|
748
|
+
const health = await (deps.verifyRestart ?? defaultVerifyRestart)(mainCommit);
|
|
749
|
+
if (health.ok && health.daemonSha && (health.daemonSha.startsWith(mainCommit) || mainCommit.startsWith(health.daemonSha)) && state) {
|
|
750
|
+
state = { ...state, canary: { ...canary, okRuns: canary.okRuns + 1 }, lastRunId: options.canaryRunId, countedRunIds: [...(state.countedRunIds ?? []), options.canaryRunId.trim()] };
|
|
751
|
+
saveEdgeRailState(stateFile, state);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const input: EdgeRailInput = {
|
|
755
|
+
// 설정에서는 세 수치만 받는다 — 여분 키가 판단 입력(seat·failureRate·canary…)을 덮지 못하게.
|
|
756
|
+
...(config.canaryOkRuns !== undefined ? { canaryOkRuns: config.canaryOkRuns } : {}),
|
|
757
|
+
...(config.failureMultiplier !== undefined ? { failureMultiplier: config.failureMultiplier } : {}),
|
|
758
|
+
...(config.minSamples !== undefined ? { minSamples: config.minSamples } : {}),
|
|
759
|
+
now: observed, mainCommit, installed: record.commit, seat: options.seat,
|
|
760
|
+
canary: state?.mainCommit === mainCommit ? state.canary : null,
|
|
761
|
+
failureRate: options.failureRate ?? (state?.mainCommit === mainCommit ? state.failureRate : null) ?? null,
|
|
762
|
+
quietWindow: options.quietWindow === true, promoted: state?.mainCommit === mainCommit ? state.promoted : false,
|
|
763
|
+
};
|
|
764
|
+
// 이 main 은 이미 되돌렸다 — 새 main 이 올 때까지 다시 깔거나 승격하지 않는다.
|
|
765
|
+
const plan = state?.mainCommit === mainCommit && state.rolledBack
|
|
766
|
+
? { decision: 'hold' as const, reason: 'this main was rolled back; waiting for a new main' }
|
|
767
|
+
: planEdgeRail(input);
|
|
768
|
+
const fixes = missingFixCommitTitles(checkout, record.commit, mainCommit, git);
|
|
769
|
+
if (!options.dryRun) try { debug.log('self-update.edge-rail', 'plan', { decision: plan.decision, reason: plan.reason, seat: options.seat, mainCommit, installed: record.commit, fixes }); } catch { /* log sink optional */ }
|
|
770
|
+
const emit = (result: SelfUpdateResult): SelfUpdateResult => {
|
|
771
|
+
const autoRollback = input.failureRate ? 'active' as const : 'inactive-no-failure-rate' as const;
|
|
772
|
+
const merged = { ...result, edgeRail: plan, missingFixes: fixes, autoRollback };
|
|
773
|
+
out.log(options.json ? JSON.stringify(merged) : `self-update --auto: ${plan.decision} — ${plan.reason} · ${fixes}${autoRollback === 'active' ? '' : ' · 자동 되돌림 비활성 — 실패율 측정 원천 없음'}`);
|
|
774
|
+
return merged;
|
|
775
|
+
};
|
|
776
|
+
if (options.dryRun || plan.decision === 'hold' || plan.decision === 'wait-quiet') return emit({ exitCode: 0, installedVersion: null, decision: null, restarted: false, reason: plan.reason });
|
|
777
|
+
if (plan.decision === 'rollback' && (!record.previous || !versionDirName(record.previous))) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: 'no previous version for rollback' });
|
|
778
|
+
if (plan.decision === 'rollback' && !deps.restartEdgeRail && liveRestartBlocked()) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: 'live restart blocked' });
|
|
779
|
+
if (plan.decision === 'rollback') {
|
|
780
|
+
const rolled = (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps);
|
|
781
|
+
if (!rolled.ok) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: rolled.reason });
|
|
782
|
+
// 되돌린 main 은 표시해 둔다 — 캐너리 OK 수가 남아 있어도 다음 실행이 다시 승격하지 못하게.
|
|
783
|
+
if (state?.mainCommit === mainCommit) saveEdgeRailState(stateFile, { ...state, promoted: false, rolledBack: true });
|
|
784
|
+
else saveEdgeRailState(stateFile, { mainCommit, canary: { seat: 'OP', okRuns: 0, startedAt: input.now.toISOString() }, rolledBack: true });
|
|
785
|
+
const restarted = await restartEdgeRail(deps, checkout);
|
|
786
|
+
const health = restarted ? await (deps.verifyRestart ?? defaultVerifyRestart)(rolled.record.commit ?? '') : null;
|
|
787
|
+
const confirmed = !!(health?.ok && health.daemonSha && rolled.record.commit
|
|
788
|
+
&& (health.daemonSha.startsWith(rolled.record.commit) || rolled.record.commit.startsWith(health.daemonSha)));
|
|
789
|
+
return emit({ exitCode: confirmed ? 0 : 1, installedVersion: rolled.versionName, decision: null, restarted, current: `versions/${rolled.versionName}`, reason: confirmed ? plan.reason : `${plan.reason}; rollback health not confirmed` });
|
|
790
|
+
}
|
|
791
|
+
if (!deps.restartEdgeRail && liveRestartBlocked()) return emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: 'live restart blocked' });
|
|
792
|
+
const installer = deps.installEdgeRail ?? ((next: SelfUpdateOptions, injected: SelfUpdateDeps) => runUpdateForInstallation(next, { cliRoot: injected.cliRoot, checkout: injected }));
|
|
793
|
+
// The existing --from dev installer checks entry existence and restores current even when install.sh fails after relinking.
|
|
794
|
+
const installed = await installer({ from: checkout, dev: true, restart: false }, { ...deps, notice: () => {}, out: { log: () => {}, error: () => {} } });
|
|
795
|
+
if (installed.exitCode !== 0) return emit(installed);
|
|
796
|
+
const nextRecord = readInstallRecord(root);
|
|
797
|
+
const nextName = versionDirName(nextRecord?.versionDir);
|
|
798
|
+
let currentLink = '';
|
|
799
|
+
try { currentLink = readlinkSync(join(root, 'current')); } catch { /* no valid installed current */ }
|
|
800
|
+
if (!nextName || !installEntryExists(root, nextName) || nextRecord?.commit !== mainCommit || currentLink !== nextRecord.versionDir) {
|
|
801
|
+
const rolled = record.versionDir && nextRecord?.previous === record.versionDir ? (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps) : null;
|
|
802
|
+
return emit({ ...installed, exitCode: 1, reason: `installed entry/commit not confirmed${rolled?.ok ? '; previous build restored' : '; rollback not confirmed'}` });
|
|
803
|
+
}
|
|
804
|
+
const restarted = await restartEdgeRail(deps, checkout);
|
|
805
|
+
// 설치한 판이 재시작·건강에서 실패해 되돌렸으면 같은 main 을 다시 깔지 않게 표시한다.
|
|
806
|
+
const markRolledBack = (): void => saveEdgeRailState(stateFile, { mainCommit, canary: { seat: 'OP', okRuns: 0, startedAt: input.now.toISOString() }, rolledBack: true });
|
|
807
|
+
if (!restarted) {
|
|
808
|
+
const rolled = record.versionDir && readInstallRecord(root)?.previous === record.versionDir
|
|
809
|
+
? (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps) : null;
|
|
810
|
+
if (rolled?.ok) markRolledBack();
|
|
811
|
+
return emit({ ...installed, exitCode: 1, restarted: false, reason: `${plan.reason}; restart not confirmed${rolled?.ok ? '; previous build restored' : '; rollback not confirmed'}` });
|
|
812
|
+
}
|
|
813
|
+
const verified = await (deps.verifyRestart ?? defaultVerifyRestart)(mainCommit);
|
|
814
|
+
if (!verified.ok || !verified.daemonSha || !(verified.daemonSha.startsWith(mainCommit) || mainCommit.startsWith(verified.daemonSha))) {
|
|
815
|
+
const rolled = record.versionDir && readInstallRecord(root)?.previous === record.versionDir
|
|
816
|
+
? (deps.rollbackEdgeRail ?? rollbackDevInstall)(deps) : null;
|
|
817
|
+
if (rolled?.ok) { markRolledBack(); await restartEdgeRail(deps, checkout); }
|
|
818
|
+
return emit({ ...installed, exitCode: 1, restarted: true, reason: `${plan.reason}; restarted build health not confirmed${rolled?.ok ? '; previous build restored' : '; rollback not confirmed'}` });
|
|
819
|
+
}
|
|
820
|
+
if (plan.decision === 'canary-install') {
|
|
821
|
+
const next: EdgeRailState = { mainCommit, canary: { seat: 'OP', okRuns: 0, startedAt: input.now.toISOString() }, ...(options.failureRate ? { failureRate: options.failureRate } : {}) };
|
|
822
|
+
saveEdgeRailState(stateFile, next);
|
|
823
|
+
} else if (state?.mainCommit === mainCommit && plan.decision === 'promote-all') {
|
|
824
|
+
saveEdgeRailState(stateFile, { ...state, promoted: true });
|
|
825
|
+
}
|
|
826
|
+
return emit({ ...installed, restarted: true });
|
|
827
|
+
}
|
|
828
|
+
|
|
631
829
|
/** Route an installed CLI by where it was installed from: a release URL → release installer, a local checkout → git update. */
|
|
632
830
|
export async function runUpdateForInstallation(
|
|
633
831
|
options: SelfUpdateOptions = {},
|
|
634
832
|
deps: { cliRoot?: string; exists?: (path: string) => boolean; readText?: (path: string) => string; release?: ReleaseUpdateDeps; checkout?: SelfUpdateDeps } = {},
|
|
635
833
|
): Promise<SelfUpdateResult> {
|
|
636
834
|
const cliRoot = resolve(deps.cliRoot ?? resolve(import.meta.dir, '../..'));
|
|
835
|
+
if (options.auto) return runEdgeRailUpdate(options, { cliRoot, ...deps.checkout });
|
|
637
836
|
const exists = deps.exists ?? existsSync;
|
|
638
837
|
const holder = resolve(cliRoot, '..', '..');
|
|
639
838
|
const parent = resolve(holder, '..');
|
|
@@ -726,7 +925,7 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
726
925
|
|
|
727
926
|
if (options.dev) {
|
|
728
927
|
try {
|
|
729
|
-
const installed = installDevBuild(checkout, head.stdout.trim(), { ...deps, runInstaller: () => run('bash', [join(checkout, 'scripts/install.sh'), '--no-modify-path'], checkout) });
|
|
928
|
+
const installed = installDevBuild(checkout, head.stdout.trim(), { ...deps, runInstaller: () => run('bash', [join(checkout, 'scripts/install.sh'), '--no-modify-path', ...(deps.installRoot ? ['--prefix', deps.installRoot] : [])], checkout) });
|
|
730
929
|
return emit({
|
|
731
930
|
exitCode: 0,
|
|
732
931
|
installedVersion: installed.versionName,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A rebuildable view of the ledger and code, never an authoritative record. */
|
|
2
|
+
export interface ContextCard {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly kind: 'task' | 'goal' | 'run' | 'pr' | 'cell' | 'doc';
|
|
5
|
+
readonly conclusion: string;
|
|
6
|
+
readonly why: string;
|
|
7
|
+
readonly verdict: string;
|
|
8
|
+
readonly remaining: readonly string[];
|
|
9
|
+
readonly pointers: readonly string[];
|
|
10
|
+
readonly source: string;
|
|
11
|
+
readonly updatedAt: string;
|
|
12
|
+
readonly supersedes?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Serialized code points, including JSON escaping; callers may supply a smaller screen budget. */
|
|
16
|
+
export const CONTEXT_CARD_SCREEN_CHARS = 1_600;
|
|
17
|
+
const length = (text: string): number => [...text].length;
|
|
18
|
+
|
|
19
|
+
/** Preserve the decision and provenance; fold only the two expandable lists. Never mutate the card. */
|
|
20
|
+
export function serializeContextCard(card: ContextCard, maxChars = CONTEXT_CARD_SCREEN_CHARS): string {
|
|
21
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 1) throw new RangeError('maxChars must be a positive safe integer');
|
|
22
|
+
if (/[\r\n\u2028\u2029]/u.test(card.conclusion)) throw new Error('conclusion must be one line');
|
|
23
|
+
const remaining = [...card.remaining];
|
|
24
|
+
const pointers = [...card.pointers];
|
|
25
|
+
const render = () => JSON.stringify({
|
|
26
|
+
id: card.id, kind: card.kind, conclusion: card.conclusion, why: card.why,
|
|
27
|
+
verdict: card.verdict, remaining: remaining.length < card.remaining.length
|
|
28
|
+
? [...remaining, `외 ${card.remaining.length - remaining.length}개`] : remaining,
|
|
29
|
+
pointers: pointers.length < card.pointers.length
|
|
30
|
+
? [...pointers, `외 ${card.pointers.length - pointers.length}개`] : pointers,
|
|
31
|
+
source: card.source, updatedAt: card.updatedAt,
|
|
32
|
+
...(card.supersedes === undefined ? {} : { supersedes: card.supersedes }),
|
|
33
|
+
});
|
|
34
|
+
let text = render();
|
|
35
|
+
while (length(text) > maxChars && (remaining.length || pointers.length)) {
|
|
36
|
+
// Remove the larger last entry first, then break ties by preferring remaining.
|
|
37
|
+
const remainingCost = remaining.length ? length(JSON.stringify(remaining.at(-1))) : -1;
|
|
38
|
+
const pointerCost = pointers.length ? length(JSON.stringify(pointers.at(-1))) : -1;
|
|
39
|
+
if (remainingCost >= pointerCost) remaining.pop();
|
|
40
|
+
else pointers.pop();
|
|
41
|
+
text = render();
|
|
42
|
+
}
|
|
43
|
+
if (length(text) > maxChars) throw new RangeError('card core and omission counts exceed maxChars');
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { RunLedgerEntry } from '../self-implement/run-ledger.js';
|
|
2
|
+
import type { ContextCard } from './index.js';
|
|
3
|
+
|
|
4
|
+
/** An injected, read-only view of the JSONL run ledger. No writer or store mutation is available. */
|
|
5
|
+
export interface TaskLedgerReader {
|
|
6
|
+
readRuns(): readonly { readonly runId: string; readonly entries: readonly RunLedgerEntry[] }[];
|
|
7
|
+
/** Returns the verbatim original ask from the goal document named by start.goalFile. */
|
|
8
|
+
readOriginalAsk?(goalFile: string): string | null;
|
|
9
|
+
/** Returns must-fix text from the review artifact named by reviewed.artifactPath. */
|
|
10
|
+
readReviewMustFix?(artifactPath: string): readonly string[] | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TaskContextCard extends ContextCard {
|
|
14
|
+
readonly kind: 'task';
|
|
15
|
+
readonly originalAsk: string;
|
|
16
|
+
readonly authoredGoal: { readonly path: string; readonly sha256: string } | null;
|
|
17
|
+
readonly runIds: readonly string[];
|
|
18
|
+
readonly mustFix: readonly string[];
|
|
19
|
+
readonly decisionCards: readonly string[];
|
|
20
|
+
readonly nextAction: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const text = (value: unknown): string | null => typeof value === 'string' && value.trim() ? value : null;
|
|
24
|
+
const strings = (value: unknown): string[] | null => Array.isArray(value) && value.every((item) => typeof item === 'string') ? [...value] : null;
|
|
25
|
+
|
|
26
|
+
/** Produce a task card from source events, not from the previous card or wall-clock time. */
|
|
27
|
+
export function buildTaskCard(taskId: string, reader: TaskLedgerReader): TaskContextCard {
|
|
28
|
+
const runs = reader.readRuns().filter((run) => run.entries.some((entry) => entry.event === 'start' && entry.goalId === taskId))
|
|
29
|
+
.map((run) => ({ runId: run.runId, entries: run.entries.filter((entry) => entry.runId === run.runId && (entry.goalId === undefined || entry.goalId === taskId)) }))
|
|
30
|
+
.sort((a, b) => a.runId < b.runId ? -1 : a.runId > b.runId ? 1 : 0);
|
|
31
|
+
if (!runs.length) throw new Error(`no run-ledger start for task: ${taskId}`);
|
|
32
|
+
const events = runs.flatMap(({ entries }) => entries).sort((a, b) =>
|
|
33
|
+
(a.timestamp ?? '').localeCompare(b.timestamp ?? '') || a.runId.localeCompare(b.runId));
|
|
34
|
+
const start = events.find((event) => event.event === 'start')!;
|
|
35
|
+
const goalFile = text(start.data.goalFile);
|
|
36
|
+
const originalAsk = text(start.data.originalAsk)
|
|
37
|
+
?? (goalFile ? reader.readOriginalAsk?.(goalFile) ?? null : null);
|
|
38
|
+
if (!originalAsk) throw new Error(`original ask unavailable for task: ${taskId}`);
|
|
39
|
+
|
|
40
|
+
const authored = [...events].reverse().find((event) => event.event === 'goal-authored'
|
|
41
|
+
&& text(event.data.path) !== null && /^[a-f0-9]{64}$/i.test(text(event.data.sha256) ?? ''));
|
|
42
|
+
const authoredGoal = authored ? { path: text(authored.data.path)!, sha256: text(authored.data.sha256)! } : null;
|
|
43
|
+
const reviews = events.filter((event) => event.event === 'reviewed' && event.data.reviewed === true);
|
|
44
|
+
const review = reviews.at(-1);
|
|
45
|
+
const artifactPath = review ? text(review.data.artifactPath) : null;
|
|
46
|
+
const mustFix = review
|
|
47
|
+
? strings(review.data.mustFix) ?? (artifactPath ? reader.readReviewMustFix?.(artifactPath) ?? null : null)
|
|
48
|
+
: [];
|
|
49
|
+
if (mustFix === null && review?.data.mustFix !== 0) throw new Error(`review must-fix unavailable for task: ${taskId}`);
|
|
50
|
+
const findings = mustFix ?? [];
|
|
51
|
+
const decisionCards = events.filter((event) => event.event === 'decision-card')
|
|
52
|
+
.flatMap((event) => {
|
|
53
|
+
const cardId = text(event.data.cardId);
|
|
54
|
+
return cardId ? [cardId] : [];
|
|
55
|
+
});
|
|
56
|
+
const nextAction = [...events].reverse().map((event) => text(event.data.nextAction)).find((value): value is string => value !== null)
|
|
57
|
+
?? findings[0] ?? null;
|
|
58
|
+
if (!nextAction) throw new Error(`next action unavailable for task: ${taskId}`);
|
|
59
|
+
const updatedAt = events.at(-1)?.timestamp;
|
|
60
|
+
if (!updatedAt) throw new Error(`timestamp unavailable for task: ${taskId}`);
|
|
61
|
+
const pointers = [
|
|
62
|
+
...(goalFile ? [goalFile] : []),
|
|
63
|
+
...(authoredGoal ? [`${authoredGoal.path}#sha256=${authoredGoal.sha256}`] : []),
|
|
64
|
+
...runs.map(({ runId }) => `run:${runId}`),
|
|
65
|
+
...(artifactPath ? [artifactPath] : []),
|
|
66
|
+
...decisionCards,
|
|
67
|
+
];
|
|
68
|
+
return {
|
|
69
|
+
id: `task:${taskId}`, kind: 'task', conclusion: nextAction,
|
|
70
|
+
why: findings.length ? `최근 리뷰 must-fix ${findings.length}개` : '원문과 런 원장에 따른 다음 행동',
|
|
71
|
+
verdict: text(review?.data.verdict) ?? '리뷰 판정 없음',
|
|
72
|
+
remaining: [...findings], pointers,
|
|
73
|
+
source: `run-ledger:${runs.map(({ runId }) => runId).join(',')}`, updatedAt,
|
|
74
|
+
originalAsk, authoredGoal, runIds: runs.map(({ runId }) => runId), mustFix: [...findings], decisionCards, nextAction,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -2442,6 +2442,8 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashComm
|
|
|
2442
2442
|
cwd: () => process.cwd(),
|
|
2443
2443
|
now: () => Date.now(),
|
|
2444
2444
|
isInteractive: () => ctx.surfaceUx !== undefined,
|
|
2445
|
+
selfResolveClarification: (await import('../../self-implement/goal-author-clarification.js')).defaultGoalAuthorSelfResolve,
|
|
2446
|
+
writeGoalDocument: (await import('../../self-implement/goal-document-write.js')).writeGoalDocumentAtomic,
|
|
2445
2447
|
buildPreflightDeps: askIo.buildAskPreflightDeps,
|
|
2446
2448
|
priorBlockSamples: () => askIo.priorBlockSamplesFrom(rows),
|
|
2447
2449
|
recentAuthoringSamples: () => askIo.recentAuthoringSamplesFrom(rows),
|
|
@@ -93,7 +93,16 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
|
|
|
93
93
|
const c = scqaField(input.scqa.c, 'c');
|
|
94
94
|
const q = input.scqa.q?.trim() ? scqaField(input.scqa.q, 'q') : undefined;
|
|
95
95
|
const a = input.scqa.a?.trim() ? scqaField(input.scqa.a, 'a') : undefined;
|
|
96
|
-
if (q && q === c) throw new Error('SCQA Q repeats C — leave Q empty');
|
|
96
|
+
if (q && q === c && input.raisedBy.agent !== 'seat-loop') throw new Error('SCQA Q repeats C — leave Q empty');
|
|
97
|
+
if (input.raisedBy.agent === 'seat-loop') {
|
|
98
|
+
if (!q || !a || !input.pendingQuestion?.trim()) throw new Error('seat-loop cards require --q, --a and --pending-question');
|
|
99
|
+
if ('skipped' in input.recommendation && !(['money', 'security', 'secret'].includes(input.category)
|
|
100
|
+
|| (['publish', 'irreversible'].includes(input.category)
|
|
101
|
+
&& /공개|게시|발행|출시|publish|release/i.test(`${title} ${input.pendingQuestion} ${input.recommendation.reason}`)
|
|
102
|
+
&& /되돌릴 수 없|철회 불가|복구 불가|irreversib|cannot (?:undo|retract)/i.test(`${input.pendingQuestion} ${input.recommendation.reason}`)))) {
|
|
103
|
+
throw new Error('seat-loop recommendation may only be skipped for money, security or irreversible publication');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
97
106
|
if (!Array.isArray(input.options) || (input.options.length < 2 && !(historical && input.options.length === 0))) throw new Error('at least two options required');
|
|
98
107
|
const options = input.options.map(o => ({ key: single(o.key, 'option key'), label: safe(single(o.label, 'option label')), consequence: safe(single(o.consequence, 'option consequence')) }));
|
|
99
108
|
if (options.some(o => !/^[a-z]$/.test(o.key)) || new Set(options.map(o => o.key)).size !== options.length) throw new Error('option keys must be unique lowercase letters');
|