elanous 0.2.12 → 0.2.13
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/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.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/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/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/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/release-loop/gate-node.ts +53 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/test-deterministic.ts +53 -3
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +12 -7
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +108 -28
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +15 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +81 -17
- package/src/harness/harness-cli-command.ts +27 -1
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +199 -7
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +3 -3
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +47 -0
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +90 -16
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +94 -10
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -23,11 +23,11 @@ export interface NodeKindEntry {
|
|
|
23
23
|
export const HARNESS_CORE_KINDS = ['agent', 'gate', 'git', 'judge', 'observe', 'hitl', 'subgraph'] as const;
|
|
24
24
|
|
|
25
25
|
// Workflow validation, the palette, and executor variant labels read the same keys.
|
|
26
|
-
export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow'] as const;
|
|
26
|
+
export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow', 'knowledge'] as const;
|
|
27
27
|
|
|
28
28
|
const descriptions: Record<GraphKind, readonly string[]> = {
|
|
29
29
|
harness: ['Run an agent', 'Check an execution gate', 'Perform git operations', 'Judge an outcome', 'Observe a result', 'Request human input', 'Run a child graph'],
|
|
30
|
-
workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow'],
|
|
30
|
+
workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow', 'Search the vault for context'],
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const kinds = new Map<GraphKind, Map<string, NodeKindEntry>>();
|
|
@@ -26,7 +26,7 @@ export function raiseApprovalCard(state: GraphRunState, pending: Pending, deps:
|
|
|
26
26
|
const nodeId = pending.nodeId;
|
|
27
27
|
const identity = { graphId: state.graphId, runId: state.runId, nodeId };
|
|
28
28
|
const visit = state.path.filter(node => node === nodeId).length;
|
|
29
|
-
if (state.dryRun || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
|
|
29
|
+
if (state.dryRun || state.growthPark || state.status !== 'awaiting-approval' || state.pending?.nodeId !== nodeId ||
|
|
30
30
|
state.pending.decision || state.path.at(-1) !== nodeId || visit < 1 ||
|
|
31
31
|
existsSync(`${join(root, 'graph-runs', state.graphId, `${state.runId}.json`)}.${state.path.length}.decision.json`)) {
|
|
32
32
|
observation('skipped', identity);
|
|
@@ -88,7 +88,7 @@ function applyOne(entry: DecisionEntry, identity: { graphId: string; runId: stri
|
|
|
88
88
|
observation('skipped', identity, entry.id, entry.choice);
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
|
-
if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.status !== 'awaiting-approval' ||
|
|
91
|
+
if (state.graphId !== graphId || state.runId !== runId || state.dryRun || state.growthPark || state.status !== 'awaiting-approval' ||
|
|
92
92
|
!state.pending || state.pending.nodeId !== nodeId || state.pending.decision || state.path.at(-1) !== nodeId ||
|
|
93
93
|
state.path.filter(node => node === nodeId).length !== visit ||
|
|
94
94
|
existsSync(`${statePath}.${state.path.length}.decision.json`)) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
3
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
1
4
|
import { createGraphVariant } from '../self-implement/graph-variant.js';
|
|
2
5
|
import type { GraphEdgeSpec, GraphNodeSpec, GraphTemplateSpec } from '../self-implement/graph-yaml.js';
|
|
3
6
|
import { createLLMGrowthProposer } from './graph-grow-llm.js';
|
|
@@ -10,6 +13,8 @@ export interface GrowthProposal {
|
|
|
10
13
|
|
|
11
14
|
export type GrowthProposer = (input: { graph: GraphTemplateSpec; nodeId: string; outcome: string; runId: string; output?: unknown }) => GrowthProposal | undefined | Promise<GrowthProposal | undefined>;
|
|
12
15
|
export type GrowthRecipeEffect = 'read-only' | 'external-effect' | 'unknown';
|
|
16
|
+
export interface GrowthCommand { command: string; dry_run_command?: string; timeout_ms?: number }
|
|
17
|
+
export interface GrowthRecipeClassification { effect: GrowthRecipeEffect; command?: GrowthCommand; approval?: string }
|
|
13
18
|
|
|
14
19
|
export interface GraphGrowth {
|
|
15
20
|
node: GraphNodeSpec;
|
|
@@ -18,8 +23,52 @@ export interface GraphGrowth {
|
|
|
18
23
|
undo: { removeNode: string; removeOutcome: { from: string; outcome: string }; removeEdge?: GraphEdgeSpec };
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
export interface GrowthDecisionDeps {
|
|
27
|
+
ledger?: Pick<DecisionLedger, 'raiseOnce'>;
|
|
28
|
+
root?: string;
|
|
29
|
+
now?: () => Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function proposalGrowth(input: Parameters<GrowthProposer>[0], proposal: GrowthProposal): GraphGrowth {
|
|
33
|
+
const edges: GraphEdgeSpec[] = [
|
|
34
|
+
{ from: input.nodeId, on: 'outcome', map: { [input.outcome]: proposal.node.nodeId } },
|
|
35
|
+
{ from: proposal.node.nodeId, to: proposal.returnTo as string },
|
|
36
|
+
];
|
|
37
|
+
return { node: proposal.node, edges, reason: proposal.reason,
|
|
38
|
+
undo: { removeNode: proposal.node.nodeId, removeOutcome: { from: input.nodeId, outcome: input.outcome }, removeEdge: edges[1]! } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The card's own ref commits to the entire growth and resolved recipe, independently of the parked run state. */
|
|
42
|
+
export function growthApprovalRef(identity: { graphId: string; runId: string; from: string; outcome: string },
|
|
43
|
+
growth: GraphGrowth, command?: GrowthCommand, approval?: string): string {
|
|
44
|
+
const hash = createHash('sha256').update(JSON.stringify({ identity, growth, command, approval })).digest('hex');
|
|
45
|
+
return `graph-growth:${hash}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function growthDecisionRef(input: Parameters<GrowthProposer>[0], proposal: GrowthProposal, command?: GrowthCommand, approval?: string): string {
|
|
49
|
+
return growthApprovalRef({ graphId: input.graph.graphId, runId: input.runId, from: input.nodeId, outcome: input.outcome },
|
|
50
|
+
proposalGrowth(input, proposal), command, approval);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function growthDecision(input: Parameters<GrowthProposer>[0], proposal: GrowthProposal, deps: GrowthDecisionDeps = {}, command?: GrowthCommand, approval?: string): DecisionEntry {
|
|
54
|
+
const ledger = deps.ledger ?? new DecisionLedger({ stateDir: deps.root ?? effectiveInstanceRoot() });
|
|
55
|
+
const ref = growthDecisionRef(input, proposal, command, approval);
|
|
56
|
+
const summary = `${proposal.node.nodeId} (${proposal.node.kind}, ${proposal.node.recipe}) → ${proposal.returnTo}`;
|
|
57
|
+
const commandDigest = command ? createHash('sha256').update(JSON.stringify(command)).digest('hex') : undefined;
|
|
58
|
+
return ledger.raiseOnce({ title: `${input.graph.graphId} / ${input.nodeId} / ${input.outcome} growth approval`, category: 'scope',
|
|
59
|
+
scqa: { s: `Graph ${input.graph.graphId}, node ${input.nodeId}, outcome ${input.outcome} has no mapped growth edge.`,
|
|
60
|
+
c: `Proposed node: ${summary}; command SHA-256: ${commandDigest ?? 'unresolved'}; reason: ${proposal.reason}`.slice(0, 240) },
|
|
61
|
+
options: [{ key: 'a', label: '승인', consequence: `Apply proposed node ${proposal.node.nodeId} and continue the run` },
|
|
62
|
+
{ key: 'b', label: '거절', consequence: 'Keep the original fallback route' }],
|
|
63
|
+
recommendation: { skipped: true, reason: 'Side-effect recipe requires a human decision' },
|
|
64
|
+
...(command || approval ? { pendingQuestion: [approval ? `Approval recipe: ${approval}` : undefined,
|
|
65
|
+
command ? `Proposed command (${commandDigest}):\n${command.command}` : undefined].filter(Boolean).join('\n') } : {}),
|
|
66
|
+
raisedBy: { agent: 'graph-runner' }, refs: [ref], dueAt: new Date((deps.now?.() ?? new Date()).getTime() + 8 * 60 * 60 * 1000).toISOString() }, ref);
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
export async function proposeGrowth(input: Parameters<GrowthProposer>[0],
|
|
22
|
-
proposer: GrowthProposer = createLLMGrowthProposer(), classifyRecipe?: (node: GraphNodeSpec) => GrowthRecipeEffect
|
|
70
|
+
proposer: GrowthProposer = createLLMGrowthProposer(), classifyRecipe?: (node: GraphNodeSpec) => GrowthRecipeEffect | GrowthRecipeClassification,
|
|
71
|
+
decisionDeps: GrowthDecisionDeps = {}): Promise<{ ok: true; graph: GraphTemplateSpec; growth: GraphGrowth } | { ok: false; reason: string; park?: boolean; growth?: GraphGrowth; decisionId?: string; decisionRef?: string; command?: GrowthCommand; approval?: string }> {
|
|
23
72
|
let proposal: GrowthProposal | undefined;
|
|
24
73
|
try { proposal = await proposer(input); }
|
|
25
74
|
catch (error) { return { ok: false, reason: `proposer failed: ${String(error)}` }; }
|
|
@@ -35,16 +84,20 @@ export async function proposeGrowth(input: Parameters<GrowthProposer>[0],
|
|
|
35
84
|
// Only a trusted classification of the resolved recipe may authorize execution.
|
|
36
85
|
let effect: GrowthRecipeEffect = 'unknown';
|
|
37
86
|
if (proposal.node.recipe !== 'none') {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
let command: GrowthCommand | undefined;
|
|
88
|
+
let approval: string | undefined;
|
|
89
|
+
try {
|
|
90
|
+
const classification = classifyRecipe?.(proposal.node) ?? 'unknown';
|
|
91
|
+
effect = typeof classification === 'string' ? classification : classification.effect;
|
|
92
|
+
command = typeof classification === 'string' ? undefined : classification.command;
|
|
93
|
+
approval = typeof classification === 'string' ? undefined : classification.approval;
|
|
94
|
+
} catch { effect = 'unknown'; }
|
|
95
|
+
if (effect !== 'read-only' || approval) {
|
|
96
|
+
const decision = growthDecision(input, proposal, decisionDeps, command, approval);
|
|
97
|
+
return { ok: false, park: true, reason: '사람 확인 필요: external or unclassified side-effect recipe',
|
|
98
|
+
decisionId: decision.id, decisionRef: growthDecisionRef(input, proposal, command, approval), command, approval,
|
|
99
|
+
growth: proposalGrowth(input, proposal) };
|
|
42
100
|
}
|
|
43
101
|
}
|
|
44
|
-
|
|
45
|
-
{ from: input.nodeId, on: 'outcome', map: { [input.outcome]: proposal.node.nodeId } },
|
|
46
|
-
{ from: proposal.node.nodeId, to: proposal.returnTo as string },
|
|
47
|
-
];
|
|
48
|
-
return { ok: true, graph: result.template, growth: { node: proposal.node, edges, reason: proposal.reason,
|
|
49
|
-
undo: { removeNode: proposal.node.nodeId, removeOutcome: { from: input.nodeId, outcome: input.outcome }, removeEdge: edges[1]! } } };
|
|
102
|
+
return { ok: true, graph: result.template, growth: proposalGrowth(input, proposal) };
|
|
50
103
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { debug } from '../debug/log.js';
|
|
5
5
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { DecisionLedger } from '../decisions/decision-ledger.js';
|
|
6
7
|
import { latestGraphRun, runGraph, type GraphRunOptions, type GraphRunState } from './runner.js';
|
|
7
8
|
|
|
8
9
|
export interface GraphTickOptions {
|
|
@@ -35,10 +36,16 @@ export async function graphTick(path: string, options: GraphTickOptions = {}): P
|
|
|
35
36
|
acquired = true;
|
|
36
37
|
await options.deps?.notify?.();
|
|
37
38
|
const latest = latestGraphRun(graphId, root);
|
|
39
|
+
const growthCard = latest?.growthPark
|
|
40
|
+
? (options.deps?.growthDecision?.list ?? ((filters: { status: 'all' }) => new DecisionLedger({ stateDir: root }).list(filters)))({ status: 'all' })
|
|
41
|
+
.find(entry => entry.id === latest.growthPark?.decisionId && entry.refs?.includes(latest.growthPark.decisionRef))
|
|
42
|
+
: undefined;
|
|
43
|
+
const growthDecided = growthCard?.status === 'decided' && growthCard.decidedBy?.kind === 'human' &&
|
|
44
|
+
(growthCard.choice === 'a' || growthCard.choice === 'b');
|
|
38
45
|
let result: GraphTickResult;
|
|
39
|
-
if (latest?.status === 'awaiting-approval' && !latest.pending?.decision) {
|
|
46
|
+
if (latest?.status === 'awaiting-approval' && !latest.pending?.decision && !growthDecided) {
|
|
40
47
|
result = { action: 'waiting', runId: latest.runId, status: latest.status };
|
|
41
|
-
} else if (latest?.status === 'running' || (latest?.status === 'awaiting-approval' && latest.pending?.decision)) {
|
|
48
|
+
} else if (latest?.status === 'running' || (latest?.status === 'awaiting-approval' && (latest.pending?.decision || growthDecided))) {
|
|
42
49
|
const state = await runGraph(path, { resumeRunId: latest.runId, deps: { ...options.deps, root } });
|
|
43
50
|
result = { action: 'resumed', runId: state.runId, status: state.status };
|
|
44
51
|
} else if (options.startIfIdle) {
|
|
@@ -11,7 +11,8 @@ import { listInstalledPlugins } from '../plugins/install/plugin-install.js';
|
|
|
11
11
|
import { credentialStatus, pluginEnv } from '../plugins/install/plugin-credentials.js';
|
|
12
12
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
13
13
|
import { loadNodeCatalog } from '../self-implement/graph-catalog.js';
|
|
14
|
-
import { proposeGrowth, type GraphGrowth, type GrowthProposer, type GrowthRecipeEffect } from './graph-grow.js';
|
|
14
|
+
import { growthApprovalRef, proposeGrowth, type GraphGrowth, type GrowthProposer, type GrowthRecipeEffect, type GrowthDecisionDeps, type GrowthCommand } from './graph-grow.js';
|
|
15
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
15
16
|
import { createLLMGrowthProposer } from './graph-grow-llm.js';
|
|
16
17
|
import { parseGraphTemplateYaml, type GraphEdgeSpec, type GraphNodeSpec, type GraphTemplateSpec } from '../self-implement/graph-yaml.js';
|
|
17
18
|
import { createGraphVariant, type GraphVariantPlan } from '../self-implement/graph-variant.js';
|
|
@@ -43,7 +44,7 @@ export interface GraphRunState {
|
|
|
43
44
|
variant?: { overlayId: string; goal: string; plan: GraphVariantPlan };
|
|
44
45
|
growth?: GraphGrowth[];
|
|
45
46
|
growthRejections?: Array<{ from: string; outcome: string; reason: string }>;
|
|
46
|
-
growthPark?: { from: string; outcome: string; reason: string };
|
|
47
|
+
growthPark?: { from: string; outcome: string; reason: string; decisionId: string; decisionRef: string; growth: GraphGrowth; command?: GrowthCommand; approval?: string };
|
|
47
48
|
resume?: { from: string; at: string; previousStatus: GraphRunState['status']; graph?: 'snapshot' | 'current' };
|
|
48
49
|
executed: number;
|
|
49
50
|
dryRun: boolean;
|
|
@@ -59,7 +60,7 @@ export interface GraphRunOptions {
|
|
|
59
60
|
resumeGraphId?: string;
|
|
60
61
|
fromNodeId?: string;
|
|
61
62
|
useCurrentGraph?: boolean;
|
|
62
|
-
deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null; growthProposer?: GrowthProposer; growthLLM?: (prompt: string) => Promise<string>; classifyGrowthRecipe?: (node: GraphNodeSpec, resolved: { command?: string; approval?: string }) => GrowthRecipeEffect };
|
|
63
|
+
deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null; growthProposer?: GrowthProposer; growthLLM?: (prompt: string) => Promise<string>; classifyGrowthRecipe?: (node: GraphNodeSpec, resolved: { command?: string; approval?: string }) => GrowthRecipeEffect; growthDecision?: GrowthDecisionDeps & { list?: (filters: { status: 'all' }) => DecisionEntry[] } };
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function safeSegment(value: string): string {
|
|
@@ -70,13 +71,16 @@ function safeSegment(value: string): string {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
type CommandRecipe = { command: string; dry_run_command?: string; timeout_ms?: number };
|
|
73
|
-
type Recipe = CommandRecipe | { approval: string };
|
|
74
|
+
type Recipe = CommandRecipe | ({ approval: string } & Partial<CommandRecipe>);
|
|
74
75
|
|
|
75
76
|
/** 접두 없는 recipe 가 카탈로그 역할이고, 그래프 옆 recipes.yaml 에 같은 키의 `{ command }` 가 있을 때만 cmd 처럼 실행한다. */
|
|
76
77
|
function roleCommand(recipe: string, recipes: Record<string, Recipe>, catalogRoles: ReadonlySet<string>): CommandRecipe | undefined {
|
|
77
78
|
if (!recipe || recipe === 'none' || recipe.includes(':') || !catalogRoles.has(recipe)) return undefined;
|
|
78
79
|
const entry = recipes[recipe];
|
|
79
|
-
|
|
80
|
+
// An entry that carries an approval only runs through `approval:` — never as a bare command.
|
|
81
|
+
return entry && !('approval' in entry) && typeof entry.command === 'string' ? { command: entry.command,
|
|
82
|
+
...(entry.dry_run_command ? { dry_run_command: entry.dry_run_command } : {}),
|
|
83
|
+
...(entry.timeout_ms ? { timeout_ms: entry.timeout_ms } : {}) } : undefined;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
function resolveNodeRecipe(node: GraphNodeSpec, recipes: Record<string, Recipe>, catalogRoles: ReadonlySet<string>): { command?: CommandRecipe; approval?: { approval: string } } {
|
|
@@ -84,12 +88,17 @@ function resolveNodeRecipe(node: GraphNodeSpec, recipes: Record<string, Recipe>,
|
|
|
84
88
|
if (node.recipe.startsWith('cmd:')) {
|
|
85
89
|
const id = node.recipe.slice(4);
|
|
86
90
|
const entry = recipes[id];
|
|
87
|
-
return entry && '
|
|
91
|
+
return entry && !('approval' in entry) && typeof entry.command === 'string' ? { command: { command: entry.command,
|
|
92
|
+
...(entry.dry_run_command ? { dry_run_command: entry.dry_run_command } : {}),
|
|
93
|
+
...(entry.timeout_ms ? { timeout_ms: entry.timeout_ms } : {}) } } : {};
|
|
88
94
|
}
|
|
89
95
|
if (node.recipe.startsWith('approval:')) {
|
|
90
96
|
const id = node.recipe.slice(9);
|
|
91
97
|
const entry = recipes[id];
|
|
92
|
-
return entry && 'approval' in entry ? { approval: entry
|
|
98
|
+
return entry && 'approval' in entry ? { approval: entry,
|
|
99
|
+
...('command' in entry && typeof entry.command === 'string' ? { command: { command: entry.command,
|
|
100
|
+
...(entry.dry_run_command ? { dry_run_command: entry.dry_run_command } : {}),
|
|
101
|
+
...(entry.timeout_ms ? { timeout_ms: entry.timeout_ms } : {}) } } : {}) } : {};
|
|
93
102
|
}
|
|
94
103
|
const command = roleCommand(node.recipe, recipes, catalogRoles);
|
|
95
104
|
return command ? { command } : {};
|
|
@@ -113,8 +122,16 @@ function recipesFor(path: string, recipeSource: string): Record<string, Recipe>
|
|
|
113
122
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`invalid recipe: ${id}`);
|
|
114
123
|
const item = raw as Record<string, unknown>;
|
|
115
124
|
if (Object.hasOwn(item, 'approval')) {
|
|
116
|
-
if (typeof item.approval !== 'string' || !item.approval.trim() ||
|
|
117
|
-
|
|
125
|
+
if (typeof item.approval !== 'string' || !item.approval.trim() ||
|
|
126
|
+
(item.command !== undefined && (typeof item.command !== 'string' || !item.command.trim())) ||
|
|
127
|
+
(item.timeout_ms !== undefined && (!Number.isSafeInteger(item.timeout_ms) || (item.timeout_ms as number) <= 0)) ||
|
|
128
|
+
(item.dry_run_command !== undefined && (typeof item.dry_run_command !== 'string' || !item.dry_run_command.trim())) ||
|
|
129
|
+
(item.command === undefined && (item.timeout_ms !== undefined || item.dry_run_command !== undefined)) ||
|
|
130
|
+
Object.keys(item).some(key => !['approval', 'command', 'dry_run_command', 'timeout_ms'].includes(key))) throw new Error(`invalid recipe: ${id}`);
|
|
131
|
+
recipes[id] = { approval: item.approval,
|
|
132
|
+
...(item.command === undefined ? {} : { command: item.command as string }),
|
|
133
|
+
...(item.dry_run_command === undefined ? {} : { dry_run_command: item.dry_run_command as string }),
|
|
134
|
+
...(item.timeout_ms === undefined ? {} : { timeout_ms: item.timeout_ms as number }) };
|
|
118
135
|
} else {
|
|
119
136
|
if (typeof item.command !== 'string' || !item.command.trim() ||
|
|
120
137
|
(item.timeout_ms !== undefined && (!Number.isSafeInteger(item.timeout_ms) || (item.timeout_ms as number) <= 0)) ||
|
|
@@ -493,8 +510,51 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
493
510
|
// Persist the restart boundary before executing it, so previous outputs remain durable.
|
|
494
511
|
persistOwnedRun(state, saved!, true);
|
|
495
512
|
}
|
|
513
|
+
let resumedGrowth = false;
|
|
496
514
|
if (options.resumeRunId && state.growthPark) {
|
|
497
|
-
|
|
515
|
+
if (state.graphId !== graphId || state.runId !== runId || (graphMode === 'current' && state.sourceHash !== approvalSourceHash)) {
|
|
516
|
+
throw new Error(`growth source changed since run was paused: ${graphId}/${runId}`);
|
|
517
|
+
}
|
|
518
|
+
const parked = state.growthPark;
|
|
519
|
+
const decision = (options.deps?.growthDecision?.list?.({ status: 'all' }) ??
|
|
520
|
+
new DecisionLedger({ stateDir: root }).list({ status: 'all' }))
|
|
521
|
+
.find(entry => entry.id === parked.decisionId && entry.refs?.includes(parked.decisionRef));
|
|
522
|
+
if (!decision || decision.status !== 'decided' || decision.decidedBy?.kind !== 'human' ||
|
|
523
|
+
(decision.choice !== 'a' && decision.choice !== 'b')) {
|
|
524
|
+
throw new Error(`growth is parked for human confirmation: ${graphId}/${runId}`);
|
|
525
|
+
}
|
|
526
|
+
if (typeof parked.decisionRef !== 'string' ||
|
|
527
|
+
growthApprovalRef({ graphId, runId, from: parked.from, outcome: parked.outcome },
|
|
528
|
+
parked.growth, parked.command, parked.approval) !== parked.decisionRef) {
|
|
529
|
+
throw new Error(`growth approval target changed since card was raised: ${graphId}/${runId}`);
|
|
530
|
+
}
|
|
531
|
+
if (state.status !== 'awaiting-approval' || state.dryRun || state.path.at(-1) !== parked.from ||
|
|
532
|
+
state.path.length !== state.nodes.length || state.nodes.at(-1)?.nodeId !== parked.from ||
|
|
533
|
+
state.pending?.nodeId !== parked.from || state.growthRejections?.at(-1)?.from !== parked.from ||
|
|
534
|
+
state.growthRejections.at(-1)?.outcome !== parked.outcome ||
|
|
535
|
+
parked.growth.undo.removeOutcome.from !== parked.from || parked.growth.undo.removeOutcome.outcome !== parked.outcome) {
|
|
536
|
+
throw new Error(`invalid parked growth: ${graphId}/${runId}`);
|
|
537
|
+
}
|
|
538
|
+
if (decision.choice === 'a') {
|
|
539
|
+
const currentCommand = resolveNodeRecipe(parked.growth.node, recipes, catalogRoles).command;
|
|
540
|
+
const currentApproval = resolveNodeRecipe(parked.growth.node, recipes, catalogRoles).approval?.approval;
|
|
541
|
+
// The approved target is the (approval, command) pair as raised — a command appearing, vanishing or changing all fail.
|
|
542
|
+
if (parked.growth.node.recipe !== 'none' &&
|
|
543
|
+
(currentApproval !== parked.approval ||
|
|
544
|
+
JSON.stringify(currentCommand ?? null) !== JSON.stringify(parked.command ?? null) ||
|
|
545
|
+
(parked.approval === undefined && !parked.command))) {
|
|
546
|
+
throw new Error(`growth command changed since approval: ${graphId}/${runId}`);
|
|
547
|
+
}
|
|
548
|
+
graph = withGrowth(graph, [parked.growth]);
|
|
549
|
+
state.growth ??= [];
|
|
550
|
+
state.growth.push(parked.growth);
|
|
551
|
+
state.growthRejections!.pop();
|
|
552
|
+
}
|
|
553
|
+
resumedGrowth = true;
|
|
554
|
+
delete state.pending;
|
|
555
|
+
delete state.growthPark;
|
|
556
|
+
state.status = 'running';
|
|
557
|
+
persistOwnedRun(state, saved!);
|
|
498
558
|
}
|
|
499
559
|
if (options.resumeRunId && state.status === 'running' && state.pending) {
|
|
500
560
|
throw new Error(`run is not awaiting approval: ${graphId}/${runId}`);
|
|
@@ -593,7 +653,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
593
653
|
let exit: number | null = null;
|
|
594
654
|
let error: string | undefined;
|
|
595
655
|
let output: unknown;
|
|
596
|
-
if (command && (!state.dryRun || command.dry_run_command) && !resumingCompleted) {
|
|
656
|
+
if (command && (!approval || state.dryRun || ok) && (!state.dryRun || command.dry_run_command) && !resumingCompleted) {
|
|
597
657
|
const outputs: Record<string, unknown> = Object.create(null);
|
|
598
658
|
// 구조 산출(마지막 JSON 줄)이 있으면 그것을, 없으면 원문을 준다 — 다음 노드가 판단을 «값»으로 받는다.
|
|
599
659
|
for (const previous of state.nodes) outputs[previous.nodeId] = lastJsonObject(previous.output) ?? previous.output ?? null;
|
|
@@ -681,7 +741,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
681
741
|
}
|
|
682
742
|
const fromNodeId = current;
|
|
683
743
|
const edge = graph.edges.find((candidate) => candidate.from === current);
|
|
684
|
-
if (document.grow === 'on' && !state.dryRun && namedOutcome !== undefined &&
|
|
744
|
+
if (!resumedGrowth && document.grow === 'on' && !state.dryRun && namedOutcome !== undefined &&
|
|
685
745
|
edge?.on === 'outcome' && edge.map && !Object.hasOwn(edge.map, namedOutcome) &&
|
|
686
746
|
!state.growth?.some((item) => item.undo.removeOutcome.from === current && item.undo.removeOutcome.outcome === namedOutcome) &&
|
|
687
747
|
!state.growthRejections?.some((item) => item.from === current && item.outcome === namedOutcome)) {
|
|
@@ -689,9 +749,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
689
749
|
: await proposeGrowth({ graph, nodeId: current, outcome: namedOutcome, runId, output: state.nodes.at(-1)?.output },
|
|
690
750
|
options.deps?.growthProposer ?? createLLMGrowthProposer(options.deps?.growthLLM), (candidate) => {
|
|
691
751
|
const resolved = resolveNodeRecipe(candidate, recipes, catalogRoles);
|
|
692
|
-
if (resolved.
|
|
693
|
-
return options.deps?.classifyGrowthRecipe?.(candidate, { command: resolved.command?.command }) ?? 'unknown'
|
|
694
|
-
|
|
752
|
+
if (!resolved.command && !resolved.approval && candidate.recipe !== 'none') return 'unknown';
|
|
753
|
+
return { effect: resolved.approval ? 'unknown' : options.deps?.classifyGrowthRecipe?.(candidate, { command: resolved.command?.command }) ?? 'unknown',
|
|
754
|
+
command: resolved.command, approval: resolved.approval?.approval };
|
|
755
|
+
}, { ...options.deps?.growthDecision, root });
|
|
695
756
|
if (result.ok) {
|
|
696
757
|
graph = result.graph;
|
|
697
758
|
state.growth ??= [];
|
|
@@ -703,10 +764,12 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
703
764
|
} else {
|
|
704
765
|
state.growthRejections ??= [];
|
|
705
766
|
state.growthRejections.push({ from: current, outcome: namedOutcome, reason: result.reason });
|
|
706
|
-
if (result.park) {
|
|
767
|
+
if (result.park && result.growth && result.decisionId && result.decisionRef) {
|
|
707
768
|
state.status = 'awaiting-approval';
|
|
708
769
|
state.pending = { nodeId: current, message: result.reason, since: new Date().toISOString() };
|
|
709
|
-
state.growthPark = { from: current, outcome: namedOutcome, reason: result.reason
|
|
770
|
+
state.growthPark = { from: current, outcome: namedOutcome, reason: result.reason,
|
|
771
|
+
growth: result.growth, decisionId: result.decisionId, decisionRef: result.decisionRef,
|
|
772
|
+
...(result.command ? { command: result.command } : {}), ...(result.approval ? { approval: result.approval } : {}) };
|
|
710
773
|
persist();
|
|
711
774
|
break;
|
|
712
775
|
}
|
|
@@ -714,6 +777,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
714
777
|
persist();
|
|
715
778
|
}
|
|
716
779
|
current = nextNode(graph.edges, current, state.dryRun && !command?.dry_run_command ? 'ok' : ok ? 'ok' : 'fail', namedOutcome);
|
|
780
|
+
resumedGrowth = false;
|
|
717
781
|
if (current !== undefined) publishInsideEvent({ kind: 'edge', graphId, runId, from: fromNodeId, to: current });
|
|
718
782
|
if (current === undefined) {
|
|
719
783
|
state.status = 'failed';
|
|
@@ -29,6 +29,7 @@ import { DEFAULT_FALLBACK_CHAIN } from '../oauth/fallback-chain.js';
|
|
|
29
29
|
import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
|
|
30
30
|
import { getUserConfig } from '../user-config.js';
|
|
31
31
|
import { installHarnessCliSinkHook } from './harness-cli-sink.js';
|
|
32
|
+
import { addHarnessQueue, listHarnessQueue, reconcileHarnessQueue, removeHarnessQueue, tickHarnessQueue, type HarnessQueueDeps } from './harness-queue.js';
|
|
32
33
|
import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from './harness-substrate-default.js';
|
|
33
34
|
import { runHarnessPlanRfc } from './harness-plan-rfc.js';
|
|
34
35
|
import { classifyGarbage, isGarbageProcessTarget, type GarbageProcess } from './process-garbage.js';
|
|
@@ -1471,7 +1472,7 @@ export function readProcessOwnership(
|
|
|
1471
1472
|
}
|
|
1472
1473
|
}
|
|
1473
1474
|
|
|
1474
|
-
function defaultListHarnessProcesses(): HarnessProcessListObservation {
|
|
1475
|
+
export function defaultListHarnessProcesses(): HarnessProcessListObservation {
|
|
1475
1476
|
let out: string;
|
|
1476
1477
|
try {
|
|
1477
1478
|
out = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu=,etime=,command='], {
|
|
@@ -2104,6 +2105,7 @@ export interface HarnessCliCommandDeps {
|
|
|
2104
2105
|
deliverableVerify?: InstallDeliverableVerifyCliDeps;
|
|
2105
2106
|
processObservation?: HarnessProcessObservationDeps;
|
|
2106
2107
|
draftSweep?: HarnessDraftSweepDeps;
|
|
2108
|
+
queue?: HarnessQueueDeps;
|
|
2107
2109
|
ask?: HarnessAskHandler;
|
|
2108
2110
|
say?: HarnessSayHandler;
|
|
2109
2111
|
/** Observe-only Pod host dispatch seam. */
|
|
@@ -2156,6 +2158,30 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
|
|
|
2156
2158
|
installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
|
|
2157
2159
|
installHarnessBudgetCommand(harnessCmd);
|
|
2158
2160
|
installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
|
|
2161
|
+
const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
|
|
2162
|
+
const queueDeps = deps.queue ?? {};
|
|
2163
|
+
const queueAction = (action: () => Promise<void>): Promise<void> => runInjectedHarnessHandler(action);
|
|
2164
|
+
queue.command('add').requiredOption('--seat <seat>', 'OP|TC|MK|UX')
|
|
2165
|
+
.option('--say <sentence>', '원문 문장').option('--ask <goal-path>', '골 문서 경로')
|
|
2166
|
+
.option('--hold', '자동 병합 금지').option('--heavy', 'Pod high 메모리')
|
|
2167
|
+
.action((opts: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }) => queueAction(async () => {
|
|
2168
|
+
const row = await addHarnessQueue(opts, queueDeps);
|
|
2169
|
+
console.log(`${row.id} ${row.seat} ${row.kind} queued`);
|
|
2170
|
+
}));
|
|
2171
|
+
queue.command('list').description('대기·발사 원장 조회').action(() => queueAction(async () => {
|
|
2172
|
+
for (const row of listHarnessQueue(queueDeps)) console.log(`${row.id} ${row.seat} ${row.status} ${row.kind} ${row.input}`);
|
|
2173
|
+
}));
|
|
2174
|
+
queue.command('remove <id>').description('대기 중 또는 종료 확인된 항목 제거').action((id: string) => queueAction(async () => {
|
|
2175
|
+
if (!await removeHarnessQueue(id, queueDeps)) throw new Error(`queue item active or not found: ${id}`);
|
|
2176
|
+
console.log(`${id} removed`);
|
|
2177
|
+
}));
|
|
2178
|
+
queue.command('reconcile <id>').description('불확정 발사를 확인하고 종료 또는 미발사 증거가 있으면 예약 해소').action((id: string) => queueAction(async () => {
|
|
2179
|
+
console.log(`${id} ${await reconcileHarnessQueue(id, queueDeps)}`);
|
|
2180
|
+
}));
|
|
2181
|
+
queue.command('tick').description('맨 앞 한 건의 자리 몫과 Pod 풀 여유를 확인하고 발사').action(() => queueAction(async () => {
|
|
2182
|
+
const result = await tickHarnessQueue(queueDeps);
|
|
2183
|
+
console.log(`${result.outcome}${result.item ? ` ${result.item.id}` : ''}: ${result.reason}`);
|
|
2184
|
+
}));
|
|
2159
2185
|
|
|
2160
2186
|
const ask = deps.ask;
|
|
2161
2187
|
if (ask) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
/** Only a confirmed terminal observation allows reconciliation to release an uncertain launch. */
|
|
6
|
+
export function writeHarnessQueueReceipt(receiptPath: string, state: 'started' | 'finished' | 'not-started'): void {
|
|
7
|
+
const temp = `${receiptPath}.${randomUUID()}.tmp`;
|
|
8
|
+
writeFileSync(temp, JSON.stringify({ state, at: new Date().toISOString() }), { flag: 'wx', mode: 0o600 });
|
|
9
|
+
renameSync(temp, receiptPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runHarnessQueueChild(
|
|
13
|
+
receiptPath: string, command: string, args: string[],
|
|
14
|
+
start: typeof spawn = spawn,
|
|
15
|
+
): Promise<number> {
|
|
16
|
+
const record = (state: 'started' | 'finished' | 'not-started') => writeHarnessQueueReceipt(receiptPath, state);
|
|
17
|
+
let child: ReturnType<typeof spawn>;
|
|
18
|
+
try { child = start(process.execPath, [command, ...args], { stdio: 'inherit', env: process.env }); }
|
|
19
|
+
catch { record('not-started'); return 1; }
|
|
20
|
+
return await new Promise<number>((done) => {
|
|
21
|
+
let started = false;
|
|
22
|
+
child.once('spawn', () => {
|
|
23
|
+
started = true;
|
|
24
|
+
record('started');
|
|
25
|
+
child.once('exit', (code) => { record('finished'); done(code ?? 1); });
|
|
26
|
+
});
|
|
27
|
+
child.once('error', () => {
|
|
28
|
+
if (!started) { record('not-started'); done(1); }
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (import.meta.main) {
|
|
34
|
+
const [receiptPath, command, ...args] = process.argv.slice(2);
|
|
35
|
+
if (!receiptPath || !command) throw new Error('queue child requires receipt path and command');
|
|
36
|
+
process.exitCode = await runHarnessQueueChild(receiptPath, command, args);
|
|
37
|
+
}
|