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
package/src/memory.ts
CHANGED
|
@@ -63,6 +63,16 @@ export function writeFileDurable(path: string, data: string, fsync: (fd: number)
|
|
|
63
63
|
|
|
64
64
|
export type MemoryType = 'user' | 'feedback' | 'project' | 'reference';
|
|
65
65
|
|
|
66
|
+
/** Context-condense metadata travels with the existing curated memory file, not a parallel store. */
|
|
67
|
+
export interface ContextMemoryCard {
|
|
68
|
+
project: string;
|
|
69
|
+
seat: string;
|
|
70
|
+
topic: string;
|
|
71
|
+
source: string;
|
|
72
|
+
updatedAt: string;
|
|
73
|
+
status: 'active' | 'retired';
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
export interface MemoryEntry {
|
|
67
77
|
id: string; // uuid v4
|
|
68
78
|
filename: string; // "<uuid>-<slug>.md"
|
|
@@ -89,6 +99,7 @@ export interface MemoryEntry {
|
|
|
89
99
|
pinned: boolean;
|
|
90
100
|
/** Optional pinned-memory freshness window; absent memories retain their original injection text. */
|
|
91
101
|
staleAfterMinutes?: number;
|
|
102
|
+
contextCard?: ContextMemoryCard;
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
// ── Paths ────────────────────────────────────────────────────────────
|
|
@@ -192,6 +203,7 @@ export interface SaveMemoryOpts {
|
|
|
192
203
|
staleAfterMinutes?: number;
|
|
193
204
|
/** Override the generated id (tests). */
|
|
194
205
|
id?: string;
|
|
206
|
+
contextCard?: ContextMemoryCard;
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): MemoryEntry {
|
|
@@ -207,6 +219,7 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
|
|
|
207
219
|
let existingPriority: number | undefined;
|
|
208
220
|
let existingPinned: boolean | undefined;
|
|
209
221
|
let existingStaleAfterMinutes: number | undefined;
|
|
222
|
+
let existingContextCard: ContextMemoryCard | undefined;
|
|
210
223
|
if (existsSync(path)) {
|
|
211
224
|
try {
|
|
212
225
|
const parsed = parseFrontmatter(readFileSync(path, 'utf-8'));
|
|
@@ -214,6 +227,7 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
|
|
|
214
227
|
if (parsed.fm.priority) existingPriority = parseInt(parsed.fm.priority, 10) || 0;
|
|
215
228
|
if (parsed.fm.pinned) existingPinned = parsed.fm.pinned === 'true';
|
|
216
229
|
existingStaleAfterMinutes = parseStaleAfterMinutes(parsed.fm['stale-after-minutes']);
|
|
230
|
+
existingContextCard = parseContextCard(parsed.fm['context-card']);
|
|
217
231
|
} catch { /* keep new createdAt */ }
|
|
218
232
|
}
|
|
219
233
|
const priority = opts.priority ?? existingPriority ?? 0;
|
|
@@ -229,6 +243,8 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
|
|
|
229
243
|
if (priority > 0) frontmatter.priority = String(Math.floor(priority));
|
|
230
244
|
if (pinned) frontmatter.pinned = 'true';
|
|
231
245
|
if (staleAfterMinutes !== undefined) frontmatter['stale-after-minutes'] = String(staleAfterMinutes);
|
|
246
|
+
const contextCard = opts.contextCard ?? existingContextCard;
|
|
247
|
+
if (contextCard) frontmatter['context-card'] = JSON.stringify(contextCard);
|
|
232
248
|
const raw = renderFrontmatter(frontmatter, opts.body);
|
|
233
249
|
writeFileDurable(path, raw);
|
|
234
250
|
|
|
@@ -248,9 +264,23 @@ export function saveMemory(opts: SaveMemoryOpts, root: string = memoryRoot()): M
|
|
|
248
264
|
priority,
|
|
249
265
|
pinned,
|
|
250
266
|
staleAfterMinutes,
|
|
267
|
+
...(contextCard ? { contextCard } : {}),
|
|
251
268
|
};
|
|
252
269
|
}
|
|
253
270
|
|
|
271
|
+
function parseContextCard(raw: string | undefined): ContextMemoryCard | undefined {
|
|
272
|
+
if (!raw) return undefined;
|
|
273
|
+
try {
|
|
274
|
+
const card: unknown = JSON.parse(raw);
|
|
275
|
+
if (card && typeof card === 'object' && !Array.isArray(card)) {
|
|
276
|
+
const c = card as Record<string, unknown>;
|
|
277
|
+
if (['project', 'seat', 'topic', 'source', 'updatedAt'].every(key => typeof c[key] === 'string')
|
|
278
|
+
&& (c.status === 'active' || c.status === 'retired')) return c as unknown as ContextMemoryCard;
|
|
279
|
+
}
|
|
280
|
+
} catch { /* malformed metadata is not a context memory */ }
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
254
284
|
/** Parse one memory file. Returns null on read/parse error. */
|
|
255
285
|
function parseMemoryFile(path: string): MemoryEntry | null {
|
|
256
286
|
if (!existsSync(path)) return null;
|
|
@@ -261,6 +291,7 @@ function parseMemoryFile(path: string): MemoryEntry | null {
|
|
|
261
291
|
const basename = path.split(/[\\/]/).pop() ?? '';
|
|
262
292
|
const match = basename.match(/^([0-9a-f-]{36})-(.+)\.md$/i);
|
|
263
293
|
const id = match ? match[1] : createHash('sha1').update(basename).digest('hex').slice(0, 36);
|
|
294
|
+
const contextCard = parseContextCard(fm['context-card']);
|
|
264
295
|
return {
|
|
265
296
|
id,
|
|
266
297
|
filename: basename,
|
|
@@ -274,6 +305,7 @@ function parseMemoryFile(path: string): MemoryEntry | null {
|
|
|
274
305
|
priority: fm.priority ? (parseInt(fm.priority, 10) || 0) : 0,
|
|
275
306
|
pinned: fm.pinned === 'true',
|
|
276
307
|
staleAfterMinutes: parseStaleAfterMinutes(fm['stale-after-minutes']),
|
|
308
|
+
...(contextCard ? { contextCard } : {}),
|
|
277
309
|
};
|
|
278
310
|
}
|
|
279
311
|
|
|
@@ -364,6 +396,7 @@ export function searchMemories(
|
|
|
364
396
|
const entries = listMemories({ type: opts.type }, root);
|
|
365
397
|
const hits: SearchHit[] = [];
|
|
366
398
|
for (const entry of entries) {
|
|
399
|
+
if (entry.contextCard?.status === 'retired') continue;
|
|
367
400
|
// 정확-토큰 매칭(2026-07-19) — 이전 substring `includes(kw)` 는 "ref"가 "reference"·
|
|
368
401
|
// "prefer" 안에까지 걸려 오차용(false-recall)을 냈다(openclaw tool-search.ts 이식:
|
|
369
402
|
// tokenize→Set.has 전체토큰 동등). name/description 은 토큰집합 정확매칭, body 는
|
|
@@ -415,7 +448,7 @@ export function rebuildIndex(root: string = memoryRoot()): void {
|
|
|
415
448
|
const byType: Record<MemoryType, MemoryEntry[]> = {
|
|
416
449
|
user: [], feedback: [], project: [], reference: [],
|
|
417
450
|
};
|
|
418
|
-
for (const e of entries) byType[e.type].push(e);
|
|
451
|
+
for (const e of entries) if (e.contextCard?.status !== 'retired') byType[e.type].push(e);
|
|
419
452
|
|
|
420
453
|
const lines: string[] = [];
|
|
421
454
|
lines.push('# elanous memory index');
|
|
@@ -515,7 +548,7 @@ export function buildMemoryInjection(
|
|
|
515
548
|
|
|
516
549
|
// 📌 pinned 기억 — 키워드 무관 항상 주입(relevance gate bypass · Letta pinned block /
|
|
517
550
|
// claude-code MEMORY.md tier 동형). priority 높은 순. core/safety/ref-location 사실용.
|
|
518
|
-
const pinnedMems = listMemories({}, root).filter(m => m.pinned).sort((a, b) => b.priority - a.priority);
|
|
551
|
+
const pinnedMems = listMemories({}, root).filter(m => m.pinned && m.contextCard?.status !== 'retired').sort((a, b) => b.priority - a.priority);
|
|
519
552
|
const pinnedParts: string[] = [];
|
|
520
553
|
for (const e of pinnedMems) {
|
|
521
554
|
const chunk = pinnedChunk(e);
|
|
@@ -678,7 +711,7 @@ export async function buildMemoryInjectionLLM(
|
|
|
678
711
|
}
|
|
679
712
|
|
|
680
713
|
// pinned 항상주입(키워드/판정 무관).
|
|
681
|
-
const pinnedMems = listMemories({}, root).filter((m) => m.pinned).sort((a, b) => b.priority - a.priority);
|
|
714
|
+
const pinnedMems = listMemories({}, root).filter((m) => m.pinned && m.contextCard?.status !== 'retired').sort((a, b) => b.priority - a.priority);
|
|
682
715
|
const pinnedParts: string[] = [];
|
|
683
716
|
for (const e of pinnedMems) {
|
|
684
717
|
const chunk = pinnedChunk(e);
|
|
@@ -693,7 +726,7 @@ export async function buildMemoryInjectionLLM(
|
|
|
693
726
|
let judged: MemoryEntry[] = [];
|
|
694
727
|
if (!lowContext) {
|
|
695
728
|
const candidates = listMemories({}, root)
|
|
696
|
-
.filter((m) => !m.pinned && !injected.includes(m.id))
|
|
729
|
+
.filter((m) => !m.pinned && m.contextCard?.status !== 'retired' && !injected.includes(m.id))
|
|
697
730
|
.slice(0, opts.maxCandidates ?? 60);
|
|
698
731
|
if (candidates.length > 0) {
|
|
699
732
|
try {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
4
|
+
import { defaultAttachmentBaseDir, resolveAttachmentPath } from '../../boot/attachment-store.js';
|
|
5
|
+
import { detectCard, NotACardError, runCardFollowup } from '../../card-followup/core.js';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import { localOcrText } from '../../telegram-card-followup.js';
|
|
9
|
+
import { jsonResponse } from './json-response.js';
|
|
10
|
+
|
|
11
|
+
export const CARD_FOLLOWUP_PATH = '/v1/card-followup';
|
|
12
|
+
|
|
13
|
+
type CardJob = { id: string; status: 'judging' | 'running' | 'done' | 'not-card' | 'failed'; updatedAt: number; replies?: [string, string, string]; error?: string };
|
|
14
|
+
type CardDeps = {
|
|
15
|
+
rootDir?: string;
|
|
16
|
+
resolveAttachment?: typeof resolveAttachmentPath;
|
|
17
|
+
ocr?: typeof localOcrText;
|
|
18
|
+
detect?: typeof detectCard;
|
|
19
|
+
run?: typeof runCardFollowup;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function withinAttachmentStore(path: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
const relativePath = relative(realpathSync(defaultAttachmentBaseDir()), realpathSync(path));
|
|
26
|
+
return relativePath !== '' && relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
27
|
+
} catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The in-memory set identifies only work launched by this daemon; disk is the pollable ledger. */
|
|
31
|
+
export function createCardFollowupJobs({
|
|
32
|
+
rootDir = effectiveInstanceRoot(),
|
|
33
|
+
resolveAttachment = resolveAttachmentPath,
|
|
34
|
+
ocr = localOcrText,
|
|
35
|
+
detect = detectCard,
|
|
36
|
+
run = runCardFollowup,
|
|
37
|
+
now = Date.now,
|
|
38
|
+
}: CardDeps = {}) {
|
|
39
|
+
const base = join(rootDir, 'graph-runs', 'card-followup');
|
|
40
|
+
const active = new Set<string>();
|
|
41
|
+
const unavailable = new Set<string>();
|
|
42
|
+
const file = (id: string) => join(base, `${id}.pwa.json`);
|
|
43
|
+
const save = (job: CardJob) => {
|
|
44
|
+
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
45
|
+
chmodSync(base, 0o700);
|
|
46
|
+
writeFileSync(file(job.id), JSON.stringify(job), { mode: 0o600 });
|
|
47
|
+
chmodSync(file(job.id), 0o600);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
async post(req: Request): Promise<Response> {
|
|
52
|
+
let body: unknown;
|
|
53
|
+
try { body = await req.json(); } catch { body = null; }
|
|
54
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
55
|
+
const { attachmentId, context } = body as Record<string, unknown>;
|
|
56
|
+
if (context !== undefined && typeof context !== 'string') return jsonResponse({ error: 'bad_request' }, 400);
|
|
57
|
+
const imagePath = typeof attachmentId === 'string' ? resolveAttachment(attachmentId) : null;
|
|
58
|
+
if (!imagePath || (resolveAttachment === resolveAttachmentPath && !withinAttachmentStore(imagePath))) {
|
|
59
|
+
return jsonResponse({ error: 'attachment-not-found' }, 400);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const id = randomUUID();
|
|
63
|
+
const update = (status: CardJob['status'], fields: Partial<Pick<CardJob, 'replies' | 'error'>> = {}) =>
|
|
64
|
+
save({ id, status, updatedAt: now(), ...fields });
|
|
65
|
+
update('judging');
|
|
66
|
+
active.add(id);
|
|
67
|
+
debug.log('card-followup.pwa', 'started', { id });
|
|
68
|
+
// Start OCR on the next event-loop turn so the acceptance response precedes the work.
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
void (async () => {
|
|
71
|
+
try {
|
|
72
|
+
const ocrText = await ocr(imagePath);
|
|
73
|
+
if (detect({ ocrText }).decision !== 'card') {
|
|
74
|
+
update('not-card');
|
|
75
|
+
debug.log('card-followup.pwa', 'not-card', { id });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
update('running');
|
|
79
|
+
const result = await run({ imagePath, ocrText, rootDir, runId: id, ...(context === undefined ? {} : { context }) });
|
|
80
|
+
update('done', { replies: result.replies });
|
|
81
|
+
debug.log('card-followup.pwa', 'done', { id });
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
const notCard = cause instanceof NotACardError;
|
|
84
|
+
// OCR·그래프 예외 문구엔 명함 원문이 섞일 수 있다 — 원장·응답엔 고정 코드만 싣는다(리뷰 must-fix).
|
|
85
|
+
try {
|
|
86
|
+
update(notCard ? 'not-card' : 'failed', notCard ? {} : { error: 'card-followup-failed' });
|
|
87
|
+
debug.log('card-followup.pwa', notCard ? 'not-card' : 'failed', { id });
|
|
88
|
+
} catch {
|
|
89
|
+
unavailable.add(id);
|
|
90
|
+
debug.log('card-followup.pwa', 'failed', { id });
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
active.delete(id);
|
|
94
|
+
}
|
|
95
|
+
})().catch(() => {
|
|
96
|
+
unavailable.add(id);
|
|
97
|
+
active.delete(id);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return jsonResponse({ id }, 202);
|
|
101
|
+
},
|
|
102
|
+
get(id: string): Response {
|
|
103
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) return jsonResponse({ error: 'not-found' }, 404);
|
|
104
|
+
if (unavailable.has(id)) return jsonResponse({ id, status: 'failed', error: 'card-followup-failed' });
|
|
105
|
+
let job: CardJob;
|
|
106
|
+
try { job = JSON.parse(readFileSync(file(id), 'utf8')) as CardJob; }
|
|
107
|
+
catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
108
|
+
if (job.id !== id) return jsonResponse({ error: 'not-found' }, 404);
|
|
109
|
+
if ((job.status === 'judging' || job.status === 'running') && !active.has(id)) {
|
|
110
|
+
return jsonResponse({ id, status: 'failed', error: 'daemon-restarted' });
|
|
111
|
+
}
|
|
112
|
+
const { status, replies, error } = job;
|
|
113
|
+
return jsonResponse({ id, status, ...(replies ? { replies } : {}), ...(error ? { error } : {}) });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type CardFollowupJobs = ReturnType<typeof createCardFollowupJobs>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { debug } from '../../debug/log.js';
|
|
2
|
+
import { DecisionLedger, type DecisionEntry } from '../../decisions/decision-ledger.js';
|
|
3
|
+
import { jsonResponse } from './json-response.js';
|
|
4
|
+
|
|
5
|
+
export const DECISIONS_PATH = '/v1/decisions';
|
|
6
|
+
|
|
7
|
+
export interface DecisionsRouteDeps {
|
|
8
|
+
authorize?: (request: Request) => boolean;
|
|
9
|
+
ledger?: Pick<DecisionLedger, 'list' | 'decide'>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function handleDecisions(req: Request, deps: DecisionsRouteDeps = {}): Promise<Response> {
|
|
13
|
+
const pathname = new URL(req.url).pathname;
|
|
14
|
+
if (pathname !== DECISIONS_PATH && !/^\/v1\/decisions\/[^/]+\/decide$/.test(pathname)) return jsonResponse({ error: 'not-found' }, 404);
|
|
15
|
+
if (!deps.authorize?.(req)) {
|
|
16
|
+
debug.log('decisions.api', 'rejected', { reason: 'unauthorized' });
|
|
17
|
+
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
18
|
+
}
|
|
19
|
+
const ledger = deps.ledger ?? new DecisionLedger();
|
|
20
|
+
if (pathname === DECISIONS_PATH) {
|
|
21
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
22
|
+
if (new URL(req.url).searchParams.get('status') !== 'open') {
|
|
23
|
+
debug.log('decisions.api', 'rejected', { reason: 'invalid-status' });
|
|
24
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
25
|
+
}
|
|
26
|
+
const decisions = ledger.list({ status: 'open' }).map(entry => ({
|
|
27
|
+
id: entry.id,
|
|
28
|
+
title: entry.title,
|
|
29
|
+
situation: entry.scqa.s.slice(0, 200),
|
|
30
|
+
options: entry.options.map(option => ({ id: option.key, label: option.label })),
|
|
31
|
+
recommendation: entry.recommendation,
|
|
32
|
+
raisedAt: entry.raisedAt,
|
|
33
|
+
dueAt: entry.dueAt,
|
|
34
|
+
}));
|
|
35
|
+
debug.log('decisions.api', 'listed', { count: decisions.length });
|
|
36
|
+
return jsonResponse({ decisions });
|
|
37
|
+
}
|
|
38
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
39
|
+
let id: string;
|
|
40
|
+
try { id = decodeURIComponent(pathname.slice(DECISIONS_PATH.length + 1, -'/decide'.length)); }
|
|
41
|
+
catch {
|
|
42
|
+
debug.log('decisions.api', 'rejected', { reason: 'invalid-id' });
|
|
43
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
44
|
+
}
|
|
45
|
+
let body: unknown;
|
|
46
|
+
try { body = await req.json(); } catch { body = null; }
|
|
47
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)
|
|
48
|
+
|| !('choice' in body) || typeof body.choice !== 'string' || !body.choice
|
|
49
|
+
|| ('note' in body && body.note !== undefined && typeof body.note !== 'string')) {
|
|
50
|
+
debug.log('decisions.api', 'rejected', { id, reason: 'invalid-body' });
|
|
51
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
52
|
+
}
|
|
53
|
+
const { choice, note } = body as { choice: string; note?: string };
|
|
54
|
+
try {
|
|
55
|
+
const entry: DecisionEntry = ledger.decide(id, choice, { kind: 'human' }, note);
|
|
56
|
+
debug.log('decisions.api', 'decided', { id });
|
|
57
|
+
return jsonResponse(entry);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : '';
|
|
60
|
+
const status = message.startsWith('decision not found:') ? 404
|
|
61
|
+
: message.startsWith('decision already closed:') ? 409
|
|
62
|
+
: message.startsWith('option not found:') || message.startsWith('note ') ? 400 : 0;
|
|
63
|
+
if (!status) throw error;
|
|
64
|
+
const reason = status === 404 ? 'not-found' : status === 409 ? 'already-decided' : 'bad_request';
|
|
65
|
+
debug.log('decisions.api', 'rejected', { id, reason });
|
|
66
|
+
return jsonResponse({ error: reason }, status);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -22,6 +22,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
22
22
|
import { isAbsolute } from 'node:path';
|
|
23
23
|
import { createProject, listProjects } from '../../project/project-store.js';
|
|
24
24
|
import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
|
|
25
|
+
import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
|
|
26
|
+
import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
|
|
27
|
+
import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
|
|
25
28
|
import { compareTokenConstTime } from '../../acp/transport/auth.js';
|
|
26
29
|
import { ensureAuthToken } from '../../auth/acp-token.js';
|
|
27
30
|
import type { NexusState } from '../state/state.js';
|
|
@@ -472,6 +475,9 @@ export interface NexusHttpServerOpts {
|
|
|
472
475
|
/** Optional installer state root for isolated API consumers and tests. */
|
|
473
476
|
pluginStateRoot?: string;
|
|
474
477
|
outputs?: OutputsDeps;
|
|
478
|
+
decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
|
|
479
|
+
cardFollowup?: CardFollowupJobs;
|
|
480
|
+
skillExec?: SkillExecJobs;
|
|
475
481
|
execRequests?: ExecRequestRunner;
|
|
476
482
|
seatRequests?: SeatRequestsDeps;
|
|
477
483
|
consultRequests?: ConsultRequestsDeps;
|
|
@@ -852,6 +858,16 @@ function execRequestRunner(): ExecRequestRunner {
|
|
|
852
858
|
return execRequestRunnerInstance;
|
|
853
859
|
}
|
|
854
860
|
|
|
861
|
+
let cardFollowupJobs: CardFollowupJobs | undefined;
|
|
862
|
+
function defaultCardFollowupJobs(): CardFollowupJobs {
|
|
863
|
+
return cardFollowupJobs ??= createCardFollowupJobs();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let skillExecJobs: SkillExecJobs | undefined;
|
|
867
|
+
function defaultSkillExecJobs(): SkillExecJobs {
|
|
868
|
+
return skillExecJobs ??= createSkillExecJobs();
|
|
869
|
+
}
|
|
870
|
+
|
|
855
871
|
export async function routeRequest(
|
|
856
872
|
req: Request,
|
|
857
873
|
opts: NexusHttpServerOpts,
|
|
@@ -1338,6 +1354,23 @@ export async function routeRequest(
|
|
|
1338
1354
|
return method === 'GET' ? handleProjectsGet() : handleProjectsPost(req);
|
|
1339
1355
|
}
|
|
1340
1356
|
|
|
1357
|
+
// Polling must remain outside the non-GET dispatch below.
|
|
1358
|
+
if (method === 'GET' && pathname.startsWith(`${SKILL_EXEC_PATH}/`) && !pathname.slice(SKILL_EXEC_PATH.length + 1).includes('/')) {
|
|
1359
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1360
|
+
return (opts.skillExec ?? defaultSkillExecJobs()).get(pathname.slice(SKILL_EXEC_PATH.length + 1));
|
|
1361
|
+
}
|
|
1362
|
+
if (method === 'GET' && pathname.startsWith(`${CARD_FOLLOWUP_PATH}/`) && !pathname.slice(CARD_FOLLOWUP_PATH.length + 1).includes('/')) {
|
|
1363
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1364
|
+
return (opts.cardFollowup ?? defaultCardFollowupJobs()).get(pathname.slice(CARD_FOLLOWUP_PATH.length + 1));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (method === 'GET' && pathname === DECISIONS_PATH) {
|
|
1368
|
+
return handleDecisions(req, {
|
|
1369
|
+
authorize: request => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
1370
|
+
...opts.decisions,
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1341
1374
|
if (method === 'GET' && pathname === '/v1/outputs') {
|
|
1342
1375
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1343
1376
|
return handleOutputsGet(req, opts.outputs);
|
|
@@ -1371,6 +1404,20 @@ export async function routeRequest(
|
|
|
1371
1404
|
// Templates POST handled here too (PR κ).
|
|
1372
1405
|
// Config / secrets PUT/POST/DELETE here too (PR μ).
|
|
1373
1406
|
if (method !== 'GET') {
|
|
1407
|
+
if (method === 'POST' && pathname === SKILL_EXEC_PATH) {
|
|
1408
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1409
|
+
return (opts.skillExec ?? defaultSkillExecJobs()).post(req);
|
|
1410
|
+
}
|
|
1411
|
+
if (method === 'POST' && pathname === CARD_FOLLOWUP_PATH) {
|
|
1412
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1413
|
+
return (opts.cardFollowup ?? defaultCardFollowupJobs()).post(req);
|
|
1414
|
+
}
|
|
1415
|
+
if (method === 'POST' && /^\/v1\/decisions\/[^/]+\/decide$/.test(pathname)) {
|
|
1416
|
+
return handleDecisions(req, {
|
|
1417
|
+
authorize: request => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
1418
|
+
...opts.decisions,
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1374
1421
|
if (method === 'POST' && pathname === '/v1/plugins/install') {
|
|
1375
1422
|
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1376
1423
|
return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveStatePersonaDir,
|
|
9
9
|
} from '../../persona/global-registry.js';
|
|
10
10
|
import { clonePreset, editPersona, loadPresets } from '../../persona/presets.js';
|
|
11
|
+
import { resolvePersonaAddress } from '../../intake-plane/persona-answer.js';
|
|
11
12
|
import type { PersonaRegistryEvent } from '../../persona/registry.js';
|
|
12
13
|
import type { PersonaProfile } from '../../persona/types.js';
|
|
13
14
|
import {
|
|
@@ -99,7 +100,7 @@ export async function handlePersonaGet(
|
|
|
99
100
|
}
|
|
100
101
|
await awaitGlobalPersonaLoad();
|
|
101
102
|
const registry = getGlobalPersonaRegistry();
|
|
102
|
-
const persona =
|
|
103
|
+
const persona = resolvePersonaAddress(personaId, registry);
|
|
103
104
|
if (!persona) return notFound(`persona ${personaId} not found`);
|
|
104
105
|
return jsonResponse({ persona: toWire(persona) }, 200);
|
|
105
106
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
6
|
+
import { dispatchSkillExec } from '../../tool-runtime/skill-exec-runtime.js';
|
|
7
|
+
import { jsonResponse } from './json-response.js';
|
|
8
|
+
|
|
9
|
+
export const SKILL_EXEC_PATH = '/v1/skills/exec';
|
|
10
|
+
|
|
11
|
+
type SkillJob = {
|
|
12
|
+
id: string;
|
|
13
|
+
skill: string;
|
|
14
|
+
status: 'running' | 'done' | 'rejected' | 'failed';
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
ok?: boolean;
|
|
17
|
+
output?: string;
|
|
18
|
+
reason?: 'not-allowlisted';
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type SkillExecDeps = {
|
|
23
|
+
rootDir?: string;
|
|
24
|
+
exec?: typeof dispatchSkillExec;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function createSkillExecJobs({ rootDir = effectiveInstanceRoot(), exec = dispatchSkillExec, now = Date.now }: SkillExecDeps = {}) {
|
|
29
|
+
const base = join(rootDir, 'skill-exec');
|
|
30
|
+
const active = new Set<string>();
|
|
31
|
+
const unavailable = new Set<string>();
|
|
32
|
+
const file = (id: string) => join(base, `${id}.json`);
|
|
33
|
+
const save = (job: SkillJob) => {
|
|
34
|
+
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
35
|
+
chmodSync(base, 0o700);
|
|
36
|
+
writeFileSync(file(job.id), JSON.stringify(job), { mode: 0o600 });
|
|
37
|
+
chmodSync(file(job.id), 0o600);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
async post(req: Request): Promise<Response> {
|
|
42
|
+
let body: unknown;
|
|
43
|
+
try { body = await req.json(); } catch { body = null; }
|
|
44
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
45
|
+
const { skill, task } = body as Record<string, unknown>;
|
|
46
|
+
if (typeof skill !== 'string' || !skill.trim() || typeof task !== 'string' || !task.trim() || task.length > 4000) {
|
|
47
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
48
|
+
}
|
|
49
|
+
const id = randomUUID();
|
|
50
|
+
const update = (status: SkillJob['status'], fields: Partial<Pick<SkillJob, 'ok' | 'output' | 'reason' | 'error'>> = {}) =>
|
|
51
|
+
save({ id, skill, status, updatedAt: now(), ...fields });
|
|
52
|
+
update('running');
|
|
53
|
+
active.add(id);
|
|
54
|
+
debug.log('skill-exec.pwa', 'started', { id, skill });
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
void (async () => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await exec({ skill, task });
|
|
59
|
+
if (!result.ok && result.output.startsWith(`자동 실행 대상 아님: ${skill}.`)) {
|
|
60
|
+
update('rejected', { reason: 'not-allowlisted' });
|
|
61
|
+
debug.log('skill-exec.pwa', 'rejected', { id, skill });
|
|
62
|
+
} else {
|
|
63
|
+
update('done', { ok: result.ok, output: result.output.slice(0, 20_000) });
|
|
64
|
+
debug.log('skill-exec.pwa', 'done', { id, skill });
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
try {
|
|
68
|
+
update('failed', { error: 'skill-exec-failed' });
|
|
69
|
+
} catch {
|
|
70
|
+
unavailable.add(id);
|
|
71
|
+
}
|
|
72
|
+
debug.log('skill-exec.pwa', 'failed', { id, skill });
|
|
73
|
+
} finally {
|
|
74
|
+
active.delete(id);
|
|
75
|
+
}
|
|
76
|
+
})().catch(() => {
|
|
77
|
+
unavailable.add(id);
|
|
78
|
+
active.delete(id);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return jsonResponse({ id }, 202);
|
|
82
|
+
},
|
|
83
|
+
get(id: string): Response {
|
|
84
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) return jsonResponse({ error: 'not-found' }, 404);
|
|
85
|
+
if (unavailable.has(id)) return jsonResponse({ id, status: 'failed', error: 'skill-exec-failed' });
|
|
86
|
+
let job: SkillJob;
|
|
87
|
+
try { job = JSON.parse(readFileSync(file(id), 'utf8')) as SkillJob; }
|
|
88
|
+
catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
89
|
+
if (job.id !== id) return jsonResponse({ error: 'not-found' }, 404);
|
|
90
|
+
if (job.status === 'running' && !active.has(id)) return jsonResponse({ id, status: 'failed', error: 'daemon-restarted' });
|
|
91
|
+
const { status, ok, output, reason, error } = job;
|
|
92
|
+
return jsonResponse({ id, status, ...(ok !== undefined ? { ok } : {}), ...(output !== undefined ? { output } : {}), ...(reason ? { reason } : {}), ...(error ? { error } : {}) });
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type SkillExecJobs = ReturnType<typeof createSkillExecJobs>;
|
package/src/persona/loader.ts
CHANGED
|
@@ -138,6 +138,22 @@ export function validatePersonaShape(
|
|
|
138
138
|
models = mr.models;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
let seat: string | undefined;
|
|
142
|
+
if (Object.hasOwn(obj, 'seat')) {
|
|
143
|
+
if (typeof obj['seat'] !== 'string' || !obj['seat'].trim()) {
|
|
144
|
+
return err(path, 'seat must be a non-empty string');
|
|
145
|
+
}
|
|
146
|
+
seat = obj['seat'];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let todo: string | undefined;
|
|
150
|
+
if (Object.hasOwn(obj, 'todo')) {
|
|
151
|
+
if (typeof obj['todo'] !== 'string' || !/^[a-z0-9_][a-z0-9_-]*\.todo\.jsonl$/i.test(obj['todo'])) {
|
|
152
|
+
return err(path, 'todo must be a per-persona .todo.jsonl filename');
|
|
153
|
+
}
|
|
154
|
+
todo = obj['todo'];
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
let mentionPatterns: readonly string[] | undefined;
|
|
142
158
|
if (obj['mentionPatterns'] !== undefined) {
|
|
143
159
|
if (!Array.isArray(obj['mentionPatterns'])
|
|
@@ -149,6 +165,8 @@ export function validatePersonaShape(
|
|
|
149
165
|
|
|
150
166
|
const profile: PersonaProfile = {
|
|
151
167
|
personaId, displayName,
|
|
168
|
+
...(seat !== undefined ? { seat } : {}),
|
|
169
|
+
...(todo !== undefined ? { todo } : {}),
|
|
152
170
|
...(description.value !== undefined ? { description: description.value } : {}),
|
|
153
171
|
...(systemPrompt.value !== undefined ? { systemPrompt: systemPrompt.value } : {}),
|
|
154
172
|
...(brand !== undefined ? { brand } : {}),
|
|
@@ -6,6 +6,8 @@ export interface PersonaTodo {
|
|
|
6
6
|
title: string;
|
|
7
7
|
status: 'open' | 'done';
|
|
8
8
|
createdAt: string;
|
|
9
|
+
priority?: number;
|
|
10
|
+
dueAt?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface PersonaTodoReadResult {
|
|
@@ -28,7 +30,9 @@ function isPersonaTodo(value: unknown): value is PersonaTodo {
|
|
|
28
30
|
&& typeof item.title === 'string' && item.title.length > 0
|
|
29
31
|
&& (item.status === 'open' || item.status === 'done')
|
|
30
32
|
&& typeof item.createdAt === 'string'
|
|
31
|
-
&& Number.isFinite(Date.parse(item.createdAt))
|
|
33
|
+
&& Number.isFinite(Date.parse(item.createdAt))
|
|
34
|
+
&& (item.priority === undefined || (typeof item.priority === 'number' && Number.isInteger(item.priority) && item.priority >= 0))
|
|
35
|
+
&& (item.dueAt === undefined || (typeof item.dueAt === 'string' && Number.isFinite(Date.parse(item.dueAt))));
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/** Missing files are empty; malformed lines are skipped and returned for observation. */
|
|
@@ -67,13 +71,14 @@ export function writePersonaTodos(dir: string, personaId: string, todos: readonl
|
|
|
67
71
|
writeFileSync(path, todos.map(todo => JSON.stringify(todo)).join('\n') + (todos.length ? '\n' : ''), 'utf8');
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
/**
|
|
74
|
+
/** Higher priority first, then earlier deadline, then earlier creation; file order breaks ties. */
|
|
75
|
+
export function orderedPersonaTodos(todos: readonly PersonaTodo[]): PersonaTodo[] {
|
|
76
|
+
return [...todos].filter((todo) => todo.status === 'open').sort((a, b) =>
|
|
77
|
+
(b.priority ?? 0) - (a.priority ?? 0)
|
|
78
|
+
|| (a.dueAt && b.dueAt ? Date.parse(a.dueAt) - Date.parse(b.dueAt) : a.dueAt ? -1 : b.dueAt ? 1 : 0)
|
|
79
|
+
|| Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
export function nextPersonaTodo(todos: readonly PersonaTodo[]): PersonaTodo | null {
|
|
72
|
-
|
|
73
|
-
for (const todo of todos) {
|
|
74
|
-
if (todo.status === 'open' && (!oldest || Date.parse(todo.createdAt) < Date.parse(oldest.createdAt))) {
|
|
75
|
-
oldest = todo;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return oldest;
|
|
83
|
+
return orderedPersonaTodos(todos)[0] ?? null;
|
|
79
84
|
}
|
package/src/persona/presets.ts
CHANGED
|
@@ -88,6 +88,7 @@ export function presetToProfileYaml(p: PersonaPreset, common: PresetCommon, opts
|
|
|
88
88
|
displayName: opts.name?.trim() ? `${opts.name.trim()} · ${p.role}` : p.displayName,
|
|
89
89
|
description: p.oneLine,
|
|
90
90
|
systemPrompt: presetSystemPrompt(p, common, name),
|
|
91
|
+
todo: `${opts.personaId}.todo.jsonl`,
|
|
91
92
|
preset: { id: p.personaId, role: p.role, ...(title ? { title } : {}), tools: p.tools.map((t) => t.name), firstQuestions: p.firstQuestions },
|
|
92
93
|
};
|
|
93
94
|
return `# 프리셋 «${p.personaId}» 에서 만든 페르소나 — elanous persona add (원본: persona-presets/${p.personaId}.yaml)\n${stringify(profile, { lineWidth: 0 })}`;
|
|
@@ -99,7 +100,7 @@ export interface PersonaWriteResult { personaId: string; displayName: string; pa
|
|
|
99
100
|
// Only fields in the runtime persona schema may be edited; provenance (`preset`) is retained, not editable.
|
|
100
101
|
const PROFILE_KEYS = new Set([
|
|
101
102
|
'personaId', 'displayName', 'description', 'systemPrompt', 'actionHosts', 'offsiteNavigation',
|
|
102
|
-
'brand', 'models', 'mentionPatterns', 'avatarUrl', 'brandColor', 'browserPort', 'residence', 'capabilities',
|
|
103
|
+
'brand', 'models', 'mentionPatterns', 'avatarUrl', 'brandColor', 'browserPort', 'residence', 'capabilities', 'todo',
|
|
103
104
|
]);
|
|
104
105
|
const NESTED_KEYS: Record<string, readonly string[]> = {
|
|
105
106
|
models: ['primary', 'fallback', 'providers'], 'models.providers': ['ollama', 'openai'],
|