elanous 0.2.11 → 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-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- 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-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.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-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.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-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.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-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.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-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.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-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- 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-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- 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/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- 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/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- 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 +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -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/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- 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 +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- 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 +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- 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 +33 -0
- package/src/llm/difficulty.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 +5 -4
- 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 +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- 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-command-job.ts +14 -10
- 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 +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- 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/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { join, isAbsolute } from 'node:path';
|
|
5
5
|
import { debug } from '../../src/debug/log.js';
|
|
6
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
|
|
7
7
|
|
|
8
8
|
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
|
|
9
|
-
|
|
9
|
+
/** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
|
|
10
|
+
export interface FeedSource { checkout: string; commit: string; deps: 'linked' | 'installed'; cleanup: () => void }
|
|
11
|
+
interface OpsUpgradeDeps { exists?: (path: string) => boolean; installSource?: () => string | null; feedSource?: (version: string) => FeedSource }
|
|
12
|
+
|
|
13
|
+
/** Detached worktree at refs/tags/v<version> with the PWA built — the feed ships what was released (10-04 0.2.11:
|
|
14
|
+
* packing process.cwd() shipped main HEAD ee689ad2 as «0.2.11» to node-b·cloud-vm instead of the cut 31129c91). */
|
|
15
|
+
export function tagFeedSource(run: CommandRunner, repo: string = process.cwd()): (version: string) => FeedSource {
|
|
16
|
+
return (version) => {
|
|
17
|
+
const tag = run('git', ['rev-parse', '--verify', `refs/tags/v${version}^{commit}`], repo);
|
|
18
|
+
const commit = tag.stdout.trim();
|
|
19
|
+
if (tag.status !== 0 || !/^[0-9a-f]{40,64}$/.test(commit)) throw new Error(`태그 없음: v${version}`);
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), 'ops-feed-'));
|
|
21
|
+
const tree = join(dir, 'tree');
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
run('git', ['worktree', 'remove', '--force', tree], repo);
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
const added = run('git', ['worktree', 'add', '--detach', tree, commit], repo);
|
|
28
|
+
if (added.status !== 0) throw new Error(`태그 체크아웃 실패: ${reason(added)}`);
|
|
29
|
+
const sameDeps = ['bun.lock', 'apps/pwa/bun.lock', 'package.json', 'apps/pwa/package.json'].every((file) => {
|
|
30
|
+
try { return readFileSync(join(repo, file), 'utf8') === readFileSync(join(tree, file), 'utf8'); }
|
|
31
|
+
catch { return false; }
|
|
32
|
+
});
|
|
33
|
+
const deps: FeedSource['deps'] = sameDeps ? 'linked' : 'installed';
|
|
34
|
+
if (sameDeps) {
|
|
35
|
+
symlinkSync(join(repo, 'node_modules'), join(tree, 'node_modules'));
|
|
36
|
+
if (existsSync(join(repo, 'apps/pwa/node_modules'))) symlinkSync(join(repo, 'apps/pwa/node_modules'), join(tree, 'apps/pwa/node_modules'));
|
|
37
|
+
} else {
|
|
38
|
+
for (const cwd of [tree, join(tree, 'apps/pwa')]) {
|
|
39
|
+
const installed = run('bun', ['install', '--frozen-lockfile'], cwd, 900_000);
|
|
40
|
+
if (installed.status !== 0) throw new Error(`태그 의존성 설치 실패: ${reason(installed)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// The feed carries the PWA build (apps/pwa/out is ignored, so a fresh tag checkout has none until built).
|
|
44
|
+
const built = run('bun', ['bin/elanous.mjs', 'nexus', 'build'], tree, 900_000);
|
|
45
|
+
if (built.status !== 0) throw new Error(`태그 PWA 빌드 실패: ${reason(built)}`);
|
|
46
|
+
return { checkout: tree, commit, deps, cleanup };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
cleanup();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
10
53
|
|
|
11
54
|
function localInstallSource(): string | null {
|
|
12
55
|
try {
|
|
@@ -54,11 +97,21 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
|
|
|
54
97
|
: (deps.exists ?? existsSync)(defaultFeed) ? defaultFeed : undefined;
|
|
55
98
|
let feedError = '';
|
|
56
99
|
if (feedPath !== undefined) {
|
|
100
|
+
let source: FeedSource | undefined;
|
|
57
101
|
try {
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
102
|
+
source = (deps.feedSource ?? tagFeedSource(run))(version);
|
|
103
|
+
const released = context.outputs['version-release']?.commit;
|
|
104
|
+
if (typeof released === 'string' && released && released !== source.commit) {
|
|
105
|
+
feedError = `내부 피드 발행 실패: 태그 v${version}(${source.commit.slice(0, 12)})가 컷(${released.slice(0, 12)})과 다르다`;
|
|
106
|
+
} else {
|
|
107
|
+
const feed = run('bun', ['scripts/publish-internal-dist.ts', '--checkout', source.checkout, '--out', feedPath]);
|
|
108
|
+
const data = lastResult(feed);
|
|
109
|
+
if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
|
|
110
|
+
else if (data.commit !== source.commit) feedError = `내부 피드 발행 실패: 묶은 커밋 ${String(data.commit).slice(0, 12)} ≠ 태그 ${source.commit.slice(0, 12)}`;
|
|
111
|
+
debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, deps: source.deps, packed: data?.commit, pwaBuild: data?.pwaBuild });
|
|
112
|
+
}
|
|
61
113
|
} catch (error) { feedError = `내부 피드 발행 실패: ${firstLine(String(error))}`; }
|
|
114
|
+
finally { source?.cleanup(); }
|
|
62
115
|
}
|
|
63
116
|
const results: HostResult[] = [];
|
|
64
117
|
for (const host of hosts as string[]) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { WARNING_ONLY } from './auto-approve-node.js';
|
|
5
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
|
|
6
7
|
|
|
7
8
|
export function publicNotes(markdown: string, pages: { pages: Array<{ id: string; slug?: string; source?: string }> }): string {
|
|
@@ -40,7 +41,9 @@ export function runPublish(run: CommandRunner = runCommand) {
|
|
|
40
41
|
const auto = context.outputs['auto-approve'];
|
|
41
42
|
if (context.outputs['approve-publish']?.outcome !== 'approved' &&
|
|
42
43
|
!(auto?.outcome === 'ok' && auto.decidedBy === 'release-loop metrics'
|
|
43
|
-
|
|
44
|
+
// The 8 blocking metrics; warning-only rows (tui-regress · mac-smoke) are shown but never counted (V1g · MAC1).
|
|
45
|
+
&& Array.isArray(auto.metrics)
|
|
46
|
+
&& auto.metrics.filter((metric: unknown) => !(metric && typeof metric === 'object' && 'name' in metric && WARNING_ONLY.has(String(metric.name)))).length === 8
|
|
44
47
|
&& auto.metrics.every((metric: unknown) => metric && typeof metric === 'object' && 'verdict' in metric && metric.verdict === 'pass'))) {
|
|
45
48
|
return { outcome: 'fail' as const, verdict: 'fail' as const, summary: 'publish blocked: neither auto-approve nor approve-publish approved' };
|
|
46
49
|
}
|
|
@@ -7,7 +7,7 @@ import { emitNodeResult } from './node-verdict.js';
|
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
8
|
|
|
9
9
|
type Kind = 'release' | 'dev-bump';
|
|
10
|
-
type Output = { outcome: 'ok' | 'error'; kind: Kind | null; version: string | null; commit: string | null; pr: number | null; worktree?: string; error?: string };
|
|
10
|
+
type Output = { outcome: 'ok' | 'error'; kind: Kind | null; version: string | null; commit: string | null; pr: number | null; files?: string[]; worktree?: string; error?: string };
|
|
11
11
|
|
|
12
12
|
export function nextDevVersion(version: string): string {
|
|
13
13
|
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
@@ -120,12 +120,22 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
|
|
|
120
120
|
const old = `"version": "${current}"`;
|
|
121
121
|
if (!text.includes(old)) throw new Error(`package.json version field not found: ${current}`);
|
|
122
122
|
writeFileSync(path, text.replace(old, `"version": "${target}"`));
|
|
123
|
+
const files = ['package.json'];
|
|
123
124
|
const serverPath = join(worktree, 'server.json');
|
|
124
125
|
if (existsSync(serverPath)) {
|
|
125
126
|
const server = JSON.parse(readFileSync(serverPath, 'utf8')) as { version: string; packages: Array<{ version: string }> };
|
|
126
127
|
server.version = target;
|
|
127
128
|
server.packages[0]!.version = target;
|
|
128
129
|
writeFileSync(serverPath, `${JSON.stringify(server, null, 2)}\n`);
|
|
130
|
+
files.push('server.json');
|
|
131
|
+
}
|
|
132
|
+
const agentPath = join(worktree, 'integrations', 'acp-registry', 'elanous', 'agent.json');
|
|
133
|
+
if (existsSync(agentPath)) {
|
|
134
|
+
const agent = JSON.parse(readFileSync(agentPath, 'utf8')) as { version: string; distribution: { npx: { package: string } } };
|
|
135
|
+
agent.version = target;
|
|
136
|
+
agent.distribution.npx.package = `elanous@${target}`;
|
|
137
|
+
writeFileSync(agentPath, `${JSON.stringify(agent, null, 2)}\n`);
|
|
138
|
+
files.push('agent.json');
|
|
129
139
|
}
|
|
130
140
|
const lockPath = join(worktree, 'bun.lock');
|
|
131
141
|
const lock = readFileSync(lockPath, 'utf8');
|
|
@@ -185,7 +195,7 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
|
|
|
185
195
|
fetchMain(repo);
|
|
186
196
|
const commit = git(repo, 'rev-parse', 'origin/main');
|
|
187
197
|
if (versionAt(repo) !== target || commit === before) throw new Error(`origin/main did not advance to ${target} after merge`);
|
|
188
|
-
const output: Output = { outcome: 'ok', kind, version: target, commit, pr: pr ? Number(pr[1]) : null };
|
|
198
|
+
const output: Output = { outcome: 'ok', kind, version: target, commit, pr: pr ? Number(pr[1]) : null, files };
|
|
189
199
|
if (kind === 'release') debug.log('release-loop.cut', 'chosen', { version: releaseVersion, source: 'main', commit });
|
|
190
200
|
git(repo, 'worktree', 'remove', '--force', worktree);
|
|
191
201
|
rmSync(temp, { recursive: true, force: true });
|
|
@@ -240,7 +250,7 @@ function main(args: string[] = process.argv.slice(2), repo = process.cwd()): Out
|
|
|
240
250
|
|
|
241
251
|
if (import.meta.main) {
|
|
242
252
|
const output = main();
|
|
243
|
-
debug.log(`release-loop.version-${output.kind ?? 'unknown'}`, 'result', { version: output.version, outcome: output.outcome });
|
|
253
|
+
debug.log(`release-loop.version-${output.kind ?? 'unknown'}`, 'result', { version: output.version, outcome: output.outcome, files: output.files ?? [] });
|
|
244
254
|
emitNodeResult({ ...output, verdict: output.outcome === 'ok' ? 'pass' : 'fail', summary: output.outcome === 'ok' ? `${output.kind} ${output.version} · ${output.commit}` : `${output.kind ?? 'version'}: ${output.error ?? 'failed'}` });
|
|
245
255
|
if (output.outcome === 'error') process.exitCode = 1;
|
|
246
256
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { TelegramClient, Api } from 'telegram';
|
|
17
17
|
import { StringSession } from 'telegram/sessions';
|
|
18
18
|
import { createInterface } from 'node:readline/promises';
|
|
19
|
+
import { parseInjectArgs } from './lib/telegram-inject-args.js';
|
|
19
20
|
|
|
20
21
|
const args = process.argv.slice(2);
|
|
21
22
|
const has = (f: string): boolean => args.includes(f);
|
|
@@ -201,15 +202,22 @@ if (has('--buttons') || has('--click') || has('--react')) {
|
|
|
201
202
|
|
|
202
203
|
// 무인 주입: SESSION STRING 으로 로그인 → 봇에게 메시지.
|
|
203
204
|
const sessionStr = process.env.TELEGRAM_USER_SESSION ?? val('--session');
|
|
204
|
-
const to = val('--to') ?? process.env.TELEGRAM_TEST_BOT ?? configBot;
|
|
205
|
-
const text = val('--text');
|
|
206
205
|
if (!sessionStr) { console.error('❌ TELEGRAM_USER_SESSION 필요 — 먼저 `--login` 으로 발급.'); process.exit(2); }
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
let inject: ReturnType<typeof parseInjectArgs>;
|
|
207
|
+
try {
|
|
208
|
+
inject = parseInjectArgs(args, { TELEGRAM_TEST_BOT: process.env.TELEGRAM_TEST_BOT ?? configBot });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error((error as Error).message);
|
|
211
|
+
process.exit(2);
|
|
212
|
+
}
|
|
213
|
+
const { to, text, photos, caption } = inject;
|
|
209
214
|
const client = new TelegramClient(new StringSession(sessionStr), apiId, apiHash, { connectionRetries: 5 });
|
|
210
215
|
await client.connect();
|
|
211
|
-
const msg =
|
|
212
|
-
|
|
216
|
+
const msg = photos.length > 0
|
|
217
|
+
? await client.sendFile(to, { file: photos.length === 1 ? photos[0]! : photos, caption })
|
|
218
|
+
: await client.sendMessage(to, { message: text! });
|
|
219
|
+
const sent = Array.isArray(msg) ? msg[0] : msg;
|
|
220
|
+
console.log(`✅ 주입 완료 → ${to} (msgId=${(sent as { id?: number } | undefined)?.id ?? '?'}). 이 계정의 텔레그램 앱에도 대화가 보입니다.`);
|
|
213
221
|
console.log(' → 데몬 처리 후 watchdog 검증: log/watchdog-stall.log (stall 없으면 A subprocess 격리 정상).');
|
|
214
222
|
await client.disconnect();
|
|
215
223
|
process.exit(0);
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
// reports the failure.
|
|
32
32
|
|
|
33
33
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
34
|
+
import { spawnSync } from 'node:child_process';
|
|
34
35
|
import { tmpdir } from 'node:os';
|
|
35
|
-
import { join } from 'node:path';
|
|
36
|
+
import { join, resolve } from 'node:path';
|
|
36
37
|
// The predicate lives in its own side-effect-free module so it can be tested:
|
|
37
38
|
// importing THIS file used to spawn the suite at load time. The runner is now
|
|
38
39
|
// behind `import.meta.main` / `runDeterministicTests()`, but the filter stays
|
|
@@ -56,6 +57,8 @@ const HARNESS_TEST_ENV_KEYS = [
|
|
|
56
57
|
// Discovery: rg -n "process\.env\.ELANOUS_(HOST|RUN|ORIGIN|SUPERVISOR|PARENT)" src
|
|
57
58
|
// These are ambient execution identities, not inputs to a deterministic test.
|
|
58
59
|
// debug.log also attributes SUBSTRATE and ARM_ID to every plain-object record.
|
|
60
|
+
const GIT_LOCATION_ENV_KEYS = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR'] as const;
|
|
61
|
+
|
|
59
62
|
const EXECUTION_ORIGIN_ENV_KEYS = [
|
|
60
63
|
'ELANOUS_HOST_ID',
|
|
61
64
|
'ELANOUS_RUN_ID',
|
|
@@ -120,6 +123,7 @@ export type DeterministicRunnerDeps = {
|
|
|
120
123
|
childGraceMs?: number;
|
|
121
124
|
killProcessGroup?: KillProcessGroup;
|
|
122
125
|
getProcessGroupId?: GetProcessGroupId;
|
|
126
|
+
gitConfig?: (cwd: string, args: string[]) => string;
|
|
123
127
|
};
|
|
124
128
|
|
|
125
129
|
export function isLivePid(pid: number): boolean {
|
|
@@ -269,11 +273,22 @@ export async function terminateDirectChild(
|
|
|
269
273
|
reportVisible(`failed to terminate child pid=${pid}`, report);
|
|
270
274
|
}
|
|
271
275
|
|
|
276
|
+
function gitConfig(cwd: string, args: string[]): string {
|
|
277
|
+
const env = { ...process.env };
|
|
278
|
+
for (const key of GIT_LOCATION_ENV_KEYS) delete env[key];
|
|
279
|
+
const result = spawnSync('git', args, { cwd, env, encoding: 'utf8' });
|
|
280
|
+
if (result.error || (result.status !== 0 && !(result.status === 1 && args.includes('--get-all') && !result.stderr))) {
|
|
281
|
+
throw new Error(`git ${args.join(' ')} failed: ${result.error?.message ?? result.stderr}`);
|
|
282
|
+
}
|
|
283
|
+
return result.stdout.trim();
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: string): NodeJS.ProcessEnv {
|
|
273
287
|
const env = Object.fromEntries(
|
|
274
288
|
Object.entries(sourceEnv).filter(([key]) => !isCredentialKey(key)
|
|
275
289
|
&& !HARNESS_TEST_ENV_KEYS.includes(key as typeof HARNESS_TEST_ENV_KEYS[number])
|
|
276
|
-
&& !EXECUTION_ORIGIN_ENV_KEYS.includes(key as typeof EXECUTION_ORIGIN_ENV_KEYS[number])
|
|
290
|
+
&& !EXECUTION_ORIGIN_ENV_KEYS.includes(key as typeof EXECUTION_ORIGIN_ENV_KEYS[number])
|
|
291
|
+
&& !GIT_LOCATION_ENV_KEYS.includes(key as typeof GIT_LOCATION_ENV_KEYS[number])),
|
|
277
292
|
);
|
|
278
293
|
env.HOME = testRoot;
|
|
279
294
|
// userConfigPath() warns whenever XDG_CONFIG_HOME is set, including when
|
|
@@ -392,6 +407,38 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
392
407
|
const report = deps.report ?? console.error;
|
|
393
408
|
const spawn = deps.spawn ?? defaultSpawn;
|
|
394
409
|
const argv = deps.argv ?? process.argv.slice(2);
|
|
410
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
411
|
+
const config = deps.gitConfig ?? gitConfig;
|
|
412
|
+
const commonDir = config(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
413
|
+
const configFile = join(resolve(cwd, commonDir), 'config');
|
|
414
|
+
const bareArgs = ['config', '--file', configFile];
|
|
415
|
+
const readBare = () => config(cwd, [...bareArgs, '--get-all', 'core.bare']);
|
|
416
|
+
const bareBefore = readBare();
|
|
417
|
+
let bareChanged = false;
|
|
418
|
+
let bareChecked = false;
|
|
419
|
+
const checkBare = (): void => {
|
|
420
|
+
bareChecked = true;
|
|
421
|
+
let bareAfter = '<unreadable>';
|
|
422
|
+
const visible = (values: string) => values ? values.replaceAll('\n', ',') : '<unset>';
|
|
423
|
+
try {
|
|
424
|
+
bareAfter = readBare();
|
|
425
|
+
if (bareAfter === bareBefore) return;
|
|
426
|
+
bareChanged = true;
|
|
427
|
+
const originalValues = bareBefore ? bareBefore.split('\n') : [];
|
|
428
|
+
if (originalValues.length > 0) {
|
|
429
|
+
config(cwd, [...bareArgs, '--replace-all', 'core.bare', originalValues[0]!]);
|
|
430
|
+
for (const value of originalValues.slice(1)) config(cwd, [...bareArgs, '--add', 'core.bare', value]);
|
|
431
|
+
} else if (bareAfter) {
|
|
432
|
+
config(cwd, [...bareArgs, '--unset-all', 'core.bare']);
|
|
433
|
+
}
|
|
434
|
+
const restored = readBare();
|
|
435
|
+
if (restored !== bareBefore) throw new Error(`expected ${visible(bareBefore)}, got ${visible(restored)}`);
|
|
436
|
+
report(`[test-deterministic] core.bare changed ${visible(bareBefore)} -> ${visible(bareAfter)}; restored ${visible(bareBefore)}; failing run`);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
bareChanged = true;
|
|
439
|
+
report(`[test-deterministic] core.bare changed ${visible(bareBefore)} -> ${visible(bareAfter)}; restore failed: ${formatError(error)}`);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
395
442
|
// ⛔ 사람이 «경로»를 직접 준 창에서는 아무것도 빼지 않는다 — 그 창은 「이것만 돌려라」다.
|
|
396
443
|
const explicitPaths = argv.some((arg) => !arg.startsWith('-'));
|
|
397
444
|
const cdpPatterns = explicitPaths ? [] : (deps.deriveCdpPatterns ?? deriveCdpTestPatterns)();
|
|
@@ -451,6 +498,7 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
451
498
|
});
|
|
452
499
|
cleanupTemporaryRoot(testRoot, { rmSync: deps.rmSync, report });
|
|
453
500
|
rootCleanupAttempted = true;
|
|
501
|
+
checkBare();
|
|
454
502
|
gate?.dispose();
|
|
455
503
|
gate = undefined;
|
|
456
504
|
if (deps.killSelf) {
|
|
@@ -465,7 +513,8 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
465
513
|
}
|
|
466
514
|
|
|
467
515
|
cleanupRoot();
|
|
468
|
-
|
|
516
|
+
checkBare();
|
|
517
|
+
return bareChanged ? 1 : (outcome.code ?? 1);
|
|
469
518
|
} catch (error) {
|
|
470
519
|
if (child) {
|
|
471
520
|
try {
|
|
@@ -484,6 +533,7 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
484
533
|
} finally {
|
|
485
534
|
gate?.dispose();
|
|
486
535
|
cleanupRoot();
|
|
536
|
+
if (!bareChecked) checkBare();
|
|
487
537
|
}
|
|
488
538
|
}
|
|
489
539
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { release } from 'node:os';
|
|
5
|
+
|
|
6
|
+
export const FILE_TIMEOUT_MS = 300_000;
|
|
7
|
+
export type Execution = { rc: number | null; stdout: string; stderr: string; secs: number; timedOut: boolean };
|
|
8
|
+
export type Executor = (file: string, timeoutMs: number) => Promise<Execution>;
|
|
9
|
+
export type Measurement = {
|
|
10
|
+
file: string; rc: number | null; pass: number | null; fail: number | null; skip: number | null;
|
|
11
|
+
secs: number; platform: string; release: string; verdict: 'pass' | 'fail' | 'skip' | '못 잼';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const TIMEOUT_EXIT_GRACE_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
// The wrapper handles SIGTERM by stopping its detached test group. Do not
|
|
17
|
+
// start another file until the wrapper closes, even after SIGKILL.
|
|
18
|
+
export function execute(
|
|
19
|
+
file: string,
|
|
20
|
+
timeoutMs: number,
|
|
21
|
+
deps: {
|
|
22
|
+
spawnChild?: typeof spawn;
|
|
23
|
+
signalGroup?: (pid: number, signal: NodeJS.Signals) => void;
|
|
24
|
+
graceMs?: number;
|
|
25
|
+
} = {},
|
|
26
|
+
): Promise<Execution> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const started = performance.now();
|
|
29
|
+
const child = (deps.spawnChild ?? spawn)('bun', ['run', 'test:deterministic', file], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
let timedOut = false;
|
|
33
|
+
let closed = false;
|
|
34
|
+
let escalation: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let terminationCheck: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
const seconds = () => Math.round((performance.now() - started) / 1000 * 100) / 100;
|
|
37
|
+
const signal = (name: NodeJS.Signals) => {
|
|
38
|
+
if (child.pid === undefined) return;
|
|
39
|
+
try { (deps.signalGroup ?? ((pid, sig) => process.kill(-pid, sig)))(child.pid, name); }
|
|
40
|
+
catch (groupError) {
|
|
41
|
+
try { child.kill(name); }
|
|
42
|
+
catch (directError) { stderr += `\n${name} failed: ${String(groupError)}; ${String(directError)}`; }
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// Bun's summary is at the end; bounded capture avoids an unbounded memory cost.
|
|
46
|
+
const collect = (previous: string, chunk: Buffer) => (previous + chunk.toString('utf8')).slice(-256_000);
|
|
47
|
+
child.stdout.on('data', (chunk: Buffer) => { stdout = collect(stdout, chunk); });
|
|
48
|
+
child.stderr.on('data', (chunk: Buffer) => { stderr = collect(stderr, chunk); });
|
|
49
|
+
const graceMs = deps.graceMs ?? TIMEOUT_EXIT_GRACE_MS;
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
timedOut = true;
|
|
52
|
+
signal('SIGTERM');
|
|
53
|
+
escalation = setTimeout(() => {
|
|
54
|
+
if (closed) return;
|
|
55
|
+
signal('SIGKILL');
|
|
56
|
+
terminationCheck = setTimeout(() => {
|
|
57
|
+
if (!closed) reject(new Error(`Cannot confirm termination of timed-out test: ${file}; stopping measurements`));
|
|
58
|
+
}, graceMs);
|
|
59
|
+
}, graceMs);
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
child.on('error', (error) => { stderr += `\n${error.message}`; });
|
|
62
|
+
child.on('close', (code) => {
|
|
63
|
+
closed = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
if (escalation) clearTimeout(escalation);
|
|
66
|
+
if (terminationCheck) clearTimeout(terminationCheck);
|
|
67
|
+
resolve({ rc: timedOut ? null : code, stdout, stderr, secs: seconds(), timedOut });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function measureList(files: readonly string[], runner: Executor = execute): Promise<Measurement[]> {
|
|
73
|
+
const measurements: Measurement[] = [];
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const result = await runner(file, FILE_TIMEOUT_MS);
|
|
76
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
77
|
+
const count = (kind: 'pass' | 'fail' | 'skip'): number | null => {
|
|
78
|
+
const matches = [...output.matchAll(new RegExp(`^\\s*(\\d+) ${kind}\\b`, 'gm'))];
|
|
79
|
+
return matches.length ? Number(matches.at(-1)![1]) : null;
|
|
80
|
+
};
|
|
81
|
+
const pass = result.timedOut ? null : count('pass');
|
|
82
|
+
const fail = result.timedOut ? null : count('fail');
|
|
83
|
+
const skip = result.timedOut ? null : count('skip');
|
|
84
|
+
const measured = pass !== null || fail !== null || skip !== null;
|
|
85
|
+
const verdict = result.timedOut || !measured || result.rc === null ? '못 잼'
|
|
86
|
+
: fail !== null && fail > 0 ? 'fail'
|
|
87
|
+
: result.rc !== 0 ? '못 잼'
|
|
88
|
+
: skip !== null && skip > 0 ? 'skip'
|
|
89
|
+
: pass !== null && pass > 0 ? 'pass' : '못 잼';
|
|
90
|
+
measurements.push({ file, rc: result.rc, pass, fail, skip, secs: result.secs, platform: process.platform, release: release(), verdict });
|
|
91
|
+
}
|
|
92
|
+
return measurements;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (import.meta.main) {
|
|
96
|
+
const args = process.argv.slice(2);
|
|
97
|
+
if (args.length < 1 || args.length > 2 || (args.length === 2 && args[1] !== '--json')) {
|
|
98
|
+
console.error('usage: bun scripts/test-diet/remeasure-list.ts <목록 파일> [--json]');
|
|
99
|
+
process.exitCode = 2;
|
|
100
|
+
} else {
|
|
101
|
+
try {
|
|
102
|
+
const files = readFileSync(args[0]!, 'utf8').split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
|
|
103
|
+
if (!files.length) throw new Error('empty test list');
|
|
104
|
+
const results = await measureList(files);
|
|
105
|
+
if (args[1] === '--json') console.log(JSON.stringify(results, null, 2));
|
|
106
|
+
else for (const row of results) console.log([row.file, row.rc ?? 'null', row.pass ?? '?', row.fail ?? '?', row.skip ?? '?', row.secs, row.platform, row.release, row.verdict].join('\t'));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(error);
|
|
109
|
+
process.exitCode = 2;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -5,6 +5,11 @@ import { Frames, launch, PUBLIC_LEAK_CHECK, sleep } from './lib/cdp.js';
|
|
|
5
5
|
import { failedApiResponse, judgeRun, judgeScene, worstSceneState, type SceneObservation, type SceneState } from './lib/rehearsal-verdict.js';
|
|
6
6
|
|
|
7
7
|
const TITLES = ['문서 아키텍처', '라이브 트레이스', '루프 에이전트', '그래프 편집기', '마법사 → 마켓', 'PTY 인텔리전스'];
|
|
8
|
+
export const EMPTY_STATE_PHRASES = ['지금 도는 런이 없습니다', '마법사가 아직 돌지 않았습니다', '터미널을 기다리는 중', '판단을 기다리는 중', '못 읽음'] as const;
|
|
9
|
+
|
|
10
|
+
export function sceneSnapshotScript(): string {
|
|
11
|
+
return `(()=>{const sections=[...document.querySelectorAll('[data-inside-scene]')]; const visible=sections.filter(s=>!s.hidden && s.getClientRects().length>0); const text=visible.length===1?visible[0].innerText:''; return {sectionsInDom:sections.length,visibleScene:visible.length===1?Number(visible[0].getAttribute('data-inside-scene')):null,textLength:text.length,emptyStates:${JSON.stringify(EMPTY_STATE_PHRASES)}.filter(phrase=>text.includes(phrase))}})()`;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
type Args = { url: string; out: string; secs: number; width: number; height: number; demo: boolean; tokenStdin: boolean };
|
|
10
15
|
|
|
@@ -168,23 +173,20 @@ export async function run(args: Args): Promise<number> {
|
|
|
168
173
|
const initialEvents = observer.take();
|
|
169
174
|
const scenes = [];
|
|
170
175
|
for (let scene = 1; scene <= 6; scene++) {
|
|
171
|
-
const hiddenByDemo =
|
|
172
|
-
if (hiddenByDemo) {
|
|
173
|
-
const result = judgeScene({ scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: 0, frames: 0, exceptions: [], failedRequests: [], textLength: 0, leaks: [], sectionsInDom: 0, visibleScene: null });
|
|
174
|
-
scenes.push(result);
|
|
175
|
-
console.log(`scene ${scene} ${result.verdict} 0s frames=0`);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
176
|
+
const hiddenByDemo = false;
|
|
178
177
|
if (scene !== 1) {
|
|
179
178
|
await cdp.ev<boolean>(`(()=>{const n=${scene}; const nav=document.querySelector('nav[aria-label="장면 선택"]'); const button=[...(nav?.querySelectorAll('button')??[])].find(b=>b.textContent?.trim().startsWith(n+' ')); button?.click(); return !!button})()`);
|
|
180
179
|
await sleep(300);
|
|
181
180
|
}
|
|
182
181
|
const frames = new Frames(join(args.out, `scene-${scene}`));
|
|
183
182
|
const states: SceneState[] = [];
|
|
183
|
+
const emptyStates = new Set<string>();
|
|
184
184
|
const sceneRect = await cdp.ev<FrameRect | null>(`(()=>{const s=document.querySelector('[data-inside-scene="${scene}"]'); if (!s || s.hidden) return null; const r=s.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height}})()`);
|
|
185
185
|
let blankFrames = 0;
|
|
186
186
|
const inspect = async () => {
|
|
187
|
-
|
|
187
|
+
const { emptyStates: observedEmpty, ...state } = await cdp.ev<SceneState & { emptyStates: string[] }>(sceneSnapshotScript());
|
|
188
|
+
states.push(state);
|
|
189
|
+
for (const phrase of observedEmpty) emptyStates.add(phrase);
|
|
188
190
|
};
|
|
189
191
|
const until = Date.now() + args.secs * 1000;
|
|
190
192
|
await inspect();
|
|
@@ -203,7 +205,7 @@ export async function run(args: Args): Promise<number> {
|
|
|
203
205
|
failedRequests: [...initialEvents.failedRequests, ...observed.failedRequests],
|
|
204
206
|
} : observed;
|
|
205
207
|
frames.toMp4(join(args.out, `scene-${scene}.mp4`), args.width, args.height);
|
|
206
|
-
const obs: SceneObservation = { scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: args.secs, frames: frames.list.length, ...events, ...dom, leaks: bad, blankFrames };
|
|
208
|
+
const obs: SceneObservation = { scene, title: TITLES[scene - 1]!, hiddenByDemo, secs: args.secs, frames: frames.list.length, ...events, ...dom, leaks: bad, blankFrames, emptyStates: [...emptyStates] };
|
|
207
209
|
const result = judgeScene(obs);
|
|
208
210
|
scenes.push(result);
|
|
209
211
|
console.log(`scene ${scene} ${result.verdict} ${args.secs}s frames=${result.frames}`);
|
|
@@ -222,13 +224,15 @@ export async function run(args: Args): Promise<number> {
|
|
|
222
224
|
'|---|---|---:|---:|---|',
|
|
223
225
|
...scenes.map((s) => `| ${s.scene} ${s.title} | ${s.verdict} | ${s.secs} | ${s.frames} | ${s.reasons.join('; ') || '없음'} |`),
|
|
224
226
|
'',
|
|
225
|
-
`ok ${result.ok} · broken ${result.broken} · 미검증 ${result.unverified}`,
|
|
227
|
+
`ok ${result.ok} · broken ${result.broken} · 미검증 ${result.unverified} · 실데이터 없음 ${result.noData}`,
|
|
228
|
+
'',
|
|
229
|
+
'종료 코드: ok=0 · broken=1 · unverified=2 · no-data=3',
|
|
226
230
|
'',
|
|
227
231
|
].join('\n'));
|
|
228
232
|
for (const line of result.banner) console.log(`⚠️ ${line}`);
|
|
229
|
-
console.log(`rehearsal ${result.verdict} ok=${result.ok} broken=${result.broken} unverified=${result.unverified}`);
|
|
230
|
-
// 0 =
|
|
231
|
-
return result.verdict === 'broken' ? 1 : result.verdict === 'unverified' ? 2 : 0;
|
|
233
|
+
console.log(`rehearsal ${result.verdict} ok=${result.ok} broken=${result.broken} unverified=${result.unverified} no-data=${result.noData}`);
|
|
234
|
+
// 0 = ok · 1 = broken · 2 = unverified · 3 = no-data.
|
|
235
|
+
return result.verdict === 'broken' ? 1 : result.verdict === 'unverified' ? 2 : result.verdict === 'no-data' ? 3 : 0;
|
|
232
236
|
} finally { observer?.close(); cdp.close(); }
|
|
233
237
|
}
|
|
234
238
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type SeedArgs = { only: 'graph' | 'wizard' | 'all'; dryRun: boolean; json: boolean };
|
|
5
|
+
export type SeedEvent = { target: 'graph' | 'wizard'; phase: 'start' | 'finish' | 'failure'; command: string[]; exitCode?: number; output?: string };
|
|
6
|
+
|
|
7
|
+
export const SCENE_SIX_GUIDANCE = '⑥ PTY 인텔리전스는 이 시드로 생성하지 않습니다. 실제 격리 미션을 실행하고 read → judge → answer 이벤트를 관측하세요.';
|
|
8
|
+
const GRAPH = 'graphs/demo/inside-seed.yaml';
|
|
9
|
+
const WIZARD_REQUEST = '데모용 자료 조사 커넥터와 스킬·그래프 초안을 만들어 주세요. 설치하거나 실행하지 마세요.';
|
|
10
|
+
const DRAFT = { description: 'Demo research draft only; no connector credentials or outbound messages.', connectors: [],
|
|
11
|
+
skill: { description: 'Demo research draft', instructions: 'Research draft only. Implement and validate processing before installing or running.', requires: [] } };
|
|
12
|
+
|
|
13
|
+
export function parseArgs(argv: string[]): SeedArgs {
|
|
14
|
+
const result: SeedArgs = { only: 'all', dryRun: false, json: false };
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const flag = argv[i]!;
|
|
18
|
+
if (!['--only', '--dry-run', '--json'].includes(flag)) throw new Error(`알 수 없는 인자: ${flag}`);
|
|
19
|
+
if (seen.has(flag)) throw new Error(`중복 인자: ${flag}`);
|
|
20
|
+
seen.add(flag);
|
|
21
|
+
if (flag === '--only') {
|
|
22
|
+
const value = argv[++i];
|
|
23
|
+
if (value !== 'all' && value !== 'graph' && value !== 'wizard') throw new Error('--only 값은 graph|wizard|all 이어야 합니다');
|
|
24
|
+
result.only = value;
|
|
25
|
+
} else if (flag === '--dry-run') result.dryRun = true;
|
|
26
|
+
else result.json = true;
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SeedDeps = {
|
|
32
|
+
repo: string;
|
|
33
|
+
run: (command: string[], cwd: string) => Promise<{ exitCode: number; output: string; stdout?: string }>;
|
|
34
|
+
prepareDraft: (repo: string) => { file: string; parent: string };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function prepareDraft(repo: string): { file: string; parent: string } {
|
|
38
|
+
const root = resolve(repo, '.elanous-test', 'demo-seed');
|
|
39
|
+
mkdirSync(root, { recursive: true });
|
|
40
|
+
const parent = mkdtempSync(join(root, 'wizard-'));
|
|
41
|
+
const file = join(parent, 'research-draft.json');
|
|
42
|
+
writeFileSync(file, JSON.stringify(DRAFT) + '\n');
|
|
43
|
+
return { file, parent };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const defaultDeps: SeedDeps = {
|
|
47
|
+
repo: resolve(import.meta.dir, '../..'),
|
|
48
|
+
prepareDraft,
|
|
49
|
+
run: async (command, cwd) => {
|
|
50
|
+
const child = Bun.spawn(command, { cwd, env: { ...process.env, NODE_ENV: 'test' }, stdout: 'pipe', stderr: 'pipe' });
|
|
51
|
+
const [output, errors, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
52
|
+
return { exitCode, stdout: output, output: [output, errors].filter(Boolean).join('\n').trim() };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export async function seed(args: SeedArgs, deps: SeedDeps = defaultDeps, onEvent?: (event: SeedEvent) => void): Promise<{ events: SeedEvent[]; guidance: string; ok: boolean }> {
|
|
57
|
+
const events: SeedEvent[] = [];
|
|
58
|
+
const emit = (event: SeedEvent): void => { events.push(event); onEvent?.(event); };
|
|
59
|
+
let ok = true;
|
|
60
|
+
for (const target of (args.only === 'all' ? ['graph', 'wizard'] : [args.only]) as Array<'graph' | 'wizard'>) {
|
|
61
|
+
const base = ['bun', 'bin/elanous.mjs', '--test'];
|
|
62
|
+
const graphCommand = [...base, 'graph', 'run', GRAPH, '--json'];
|
|
63
|
+
const wizardCommand = [...base, 'plugin', 'make', WIZARD_REQUEST, '--name', 'inside-demo-draft', '--draft-file', '<isolated-draft-file>', '--dir', '<isolated-parent>', '--json'];
|
|
64
|
+
const command = target === 'graph' ? graphCommand : wizardCommand;
|
|
65
|
+
if (args.dryRun) {
|
|
66
|
+
emit({ target, phase: 'start', command });
|
|
67
|
+
emit({ target, phase: 'finish', command, exitCode: 0, output: 'dry-run: no process started' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (target === 'wizard') {
|
|
72
|
+
const { file, parent } = deps.prepareDraft(deps.repo);
|
|
73
|
+
command.splice(command.indexOf('<isolated-draft-file>'), 1, file);
|
|
74
|
+
command.splice(command.indexOf('<isolated-parent>'), 1, parent);
|
|
75
|
+
}
|
|
76
|
+
emit({ target, phase: 'start', command });
|
|
77
|
+
const result = await deps.run(command, deps.repo);
|
|
78
|
+
const parsed = (() => {
|
|
79
|
+
try { return JSON.parse(result.stdout ?? result.output) as { status?: string; dryRun?: boolean; errors?: string[]; timings?: { install?: number } }; } catch { return null; }
|
|
80
|
+
})();
|
|
81
|
+
const complete = target === 'wizard'
|
|
82
|
+
? parsed?.status === 'draft' && parsed.timings?.install === 0 && parsed.errors?.length === 0
|
|
83
|
+
: parsed?.status === 'done' && parsed.dryRun === false;
|
|
84
|
+
const phase = result.exitCode === 0 && complete ? 'finish' : 'failure';
|
|
85
|
+
emit({ target, phase, command, exitCode: result.exitCode, output: result.output });
|
|
86
|
+
if (phase === 'failure') ok = false;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
ok = false;
|
|
89
|
+
emit({ target, phase: 'failure', command, output: error instanceof Error ? error.message : String(error) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { events, guidance: SCENE_SIX_GUIDANCE, ok };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (import.meta.main) {
|
|
96
|
+
try {
|
|
97
|
+
const args = parseArgs(process.argv.slice(2));
|
|
98
|
+
const report = await seed(args, defaultDeps, args.json ? undefined : (event) => {
|
|
99
|
+
console.log(`${event.target} ${event.phase}${event.exitCode === undefined ? '' : ` (exit ${event.exitCode})`}: ${event.command.join(' ')}${event.output ? `\n${event.output}` : ''}`);
|
|
100
|
+
});
|
|
101
|
+
if (args.json) console.log(JSON.stringify(report));
|
|
102
|
+
else console.log(report.guidance);
|
|
103
|
+
if (!report.ok) process.exitCode = 1;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
}
|