@ludi-uni/ludi-agent-kit 0.1.0

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.
Files changed (172) hide show
  1. package/AGENTS.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/adapters/codex/README.md +24 -0
  5. package/adapters/codex/skill-metadata/visual-verification/agents/openai.yaml +7 -0
  6. package/adapters/pi/README.md +88 -0
  7. package/adapters/pi/browser/agent-browser.mjs +193 -0
  8. package/adapters/pi/lib/invoke.mjs +55 -0
  9. package/adapters/pi/lib/list-models.mjs +29 -0
  10. package/adapters/pi/lib/settings-proposal.mjs +34 -0
  11. package/adapters/pi/lib/subagent.mjs +175 -0
  12. package/adapters/pi/loop-guard/index.js +51 -0
  13. package/adapters/pi/maintenance-policy.json +36 -0
  14. package/adapters/pi/mcp.template.json +4 -0
  15. package/adapters/pi/model-catalog.json +97 -0
  16. package/adapters/pi/models.json +13 -0
  17. package/adapters/pi/models.local.example.json +14 -0
  18. package/adapters/pi/orchestrator-ext/command.mjs +14 -0
  19. package/adapters/pi/orchestrator-ext/index.js +150 -0
  20. package/adapters/pi/settings.template.json +7 -0
  21. package/adapters/pi/shell-gate/index.js +70 -0
  22. package/adapters/pi/sync-pi.ps1 +137 -0
  23. package/agents/README.md +26 -0
  24. package/agents/browser.md +64 -0
  25. package/agents/coder.md +31 -0
  26. package/agents/orchestrator.md +37 -0
  27. package/agents/reviewer.md +32 -0
  28. package/agents/scout.md +35 -0
  29. package/agents/tester.md +28 -0
  30. package/agents/visual.md +28 -0
  31. package/context-pack/SPEC.md +101 -0
  32. package/context-pack/context-pack.schema.json +79 -0
  33. package/context-pack/examples/example-fix.md +44 -0
  34. package/docs/architecture.md +55 -0
  35. package/docs/migration-from-codex-setting.md +44 -0
  36. package/docs/model-maintenance.md +401 -0
  37. package/docs/orchestrator.md +155 -0
  38. package/docs/phase2-report.md +39 -0
  39. package/docs/roadmap.md +27 -0
  40. package/docs/third-party.md +15 -0
  41. package/lib/agents.mjs +79 -0
  42. package/lib/context-pack.mjs +215 -0
  43. package/lib/job.mjs +312 -0
  44. package/lib/language-policy.mjs +27 -0
  45. package/lib/maintenance-exec.mjs +377 -0
  46. package/lib/maintenance-runner.mjs +266 -0
  47. package/lib/maintenance.mjs +422 -0
  48. package/lib/normalize.mjs +101 -0
  49. package/lib/observe/differ.mjs +185 -0
  50. package/lib/observe/observation.mjs +147 -0
  51. package/lib/observe/observers.mjs +134 -0
  52. package/lib/observe/sources.mjs +154 -0
  53. package/lib/orchestrator/activity.mjs +249 -0
  54. package/lib/orchestrator/api.mjs +151 -0
  55. package/lib/orchestrator/contract.mjs +68 -0
  56. package/lib/orchestrator/escalation.mjs +84 -0
  57. package/lib/orchestrator/evaluator.mjs +92 -0
  58. package/lib/orchestrator/failures.mjs +88 -0
  59. package/lib/orchestrator/health.mjs +53 -0
  60. package/lib/orchestrator/orchestrator.mjs +483 -0
  61. package/lib/orchestrator/permissions.mjs +64 -0
  62. package/lib/orchestrator/planner.mjs +194 -0
  63. package/lib/orchestrator/policy.mjs +134 -0
  64. package/lib/orchestrator/router.mjs +45 -0
  65. package/lib/orchestrator/runner.mjs +278 -0
  66. package/lib/orchestrator/shell-policy.mjs +52 -0
  67. package/lib/orchestrator/store.mjs +581 -0
  68. package/lib/orchestrator/task-store.mjs +79 -0
  69. package/lib/orchestrator/turn-budget.mjs +63 -0
  70. package/lib/orchestrator/worktree.mjs +72 -0
  71. package/lib/pipeline.mjs +279 -0
  72. package/lib/registry.mjs +63 -0
  73. package/lib/resolve.mjs +35 -0
  74. package/lib/routing.mjs +137 -0
  75. package/lib/telemetry.mjs +222 -0
  76. package/mcp/README.md +11 -0
  77. package/mcp/servers.json +13 -0
  78. package/orchestration/decision-policy.json +66 -0
  79. package/package.json +56 -0
  80. package/routing/README.md +24 -0
  81. package/routing/routing.json +81 -0
  82. package/routing/routing.schema.json +66 -0
  83. package/rules/README.md +10 -0
  84. package/rules/common.md +52 -0
  85. package/rules/loop-prevention.md +15 -0
  86. package/rules/repo-local.md +6 -0
  87. package/scripts/check-environment.ps1 +22 -0
  88. package/scripts/context-pack.mjs +17 -0
  89. package/scripts/e2e-investigate-repro.mjs +66 -0
  90. package/scripts/model-maintenance-job.mjs +59 -0
  91. package/scripts/observe-models.mjs +97 -0
  92. package/scripts/orchestrate.mjs +137 -0
  93. package/scripts/reevaluate-models.mjs +95 -0
  94. package/scripts/report-model-maintenance.mjs +70 -0
  95. package/scripts/resolve-capabilities.mjs +39 -0
  96. package/scripts/run-pipeline.mjs +56 -0
  97. package/scripts/sync-agents-md.ps1 +10 -0
  98. package/scripts/validate.mjs +71 -0
  99. package/skills/README.md +14 -0
  100. package/skills/pi-workflow/SKILL.md +26 -0
  101. package/skills/pi-workflow/references/code-investigation-and-fix.md +16 -0
  102. package/skills/pi-workflow/references/research.md +14 -0
  103. package/skills/pi-workflow/references/review.md +11 -0
  104. package/skills/pi-workflow/references/visual-work.md +14 -0
  105. package/skills/project-management/SKILL.md +106 -0
  106. package/skills/project-management/references/operations.md +52 -0
  107. package/skills/visual-verification/SKILL.md +88 -0
  108. package/skills/visual-verification/scripts/analyze-speech.ps1 +346 -0
  109. package/skills/visual-verification/scripts/backends/whisperx_backend.py +234 -0
  110. package/skills/visual-verification/scripts/common.ps1 +387 -0
  111. package/skills/visual-verification/scripts/contact-sheet.ps1 +121 -0
  112. package/skills/visual-verification/scripts/desktop-discover.ps1 +45 -0
  113. package/skills/visual-verification/scripts/desktop-inspect.ps1 +67 -0
  114. package/skills/visual-verification/scripts/desktop-record.ps1 +97 -0
  115. package/skills/visual-verification/scripts/desktop-screenshot.ps1 +65 -0
  116. package/skills/visual-verification/scripts/evaluate-sync.ps1 +249 -0
  117. package/skills/visual-verification/scripts/extract-frames.ps1 +79 -0
  118. package/skills/visual-verification/scripts/inspect-media.ps1 +138 -0
  119. package/skills/visual-verification/scripts/record-av.ps1 +102 -0
  120. package/skills/visual-verification/scripts/record.ps1 +72 -0
  121. package/skills/visual-verification/scripts/screenshot.ps1 +44 -0
  122. package/skills/visual-verification/scripts/waveform.ps1 +450 -0
  123. package/skills/visual-verification/scripts/winapp-common.ps1 +465 -0
  124. package/tests/activity.test.mjs +252 -0
  125. package/tests/attempt-budget.test.mjs +102 -0
  126. package/tests/browser.test.mjs +121 -0
  127. package/tests/context-pack.test.mjs +98 -0
  128. package/tests/dirty-gate.test.mjs +211 -0
  129. package/tests/e2e-browser.mjs +66 -0
  130. package/tests/e2e-real-orchestrator-resume.mjs +101 -0
  131. package/tests/e2e-real-orchestrator.mjs +41 -0
  132. package/tests/e2e-real-pi.mjs +27 -0
  133. package/tests/e2e-real-tool-orchestrator.mjs +66 -0
  134. package/tests/fixtures/browser-page/index.html +20 -0
  135. package/tests/fixtures/maintenance/availability.txt +5 -0
  136. package/tests/fixtures/maintenance/catalog.json +74 -0
  137. package/tests/fixtures/maintenance/events.json +13 -0
  138. package/tests/fixtures/math-repo/README.md +3 -0
  139. package/tests/fixtures/math-repo/package.json +7 -0
  140. package/tests/fixtures/math-repo/src/math.js +11 -0
  141. package/tests/fixtures/math-repo/test/math.test.js +7 -0
  142. package/tests/fixtures/observe/announcements.json +8 -0
  143. package/tests/fixtures/orch-concurrent-child.mjs +44 -0
  144. package/tests/fixtures/orch-persist-child.mjs +61 -0
  145. package/tests/job.test.mjs +230 -0
  146. package/tests/kit.test.mjs +79 -0
  147. package/tests/language-policy.test.mjs +93 -0
  148. package/tests/loop-guard.test.mjs +60 -0
  149. package/tests/maintenance-exec.test.mjs +218 -0
  150. package/tests/maintenance-runner.test.mjs +222 -0
  151. package/tests/maintenance.test.mjs +195 -0
  152. package/tests/observe.test.mjs +283 -0
  153. package/tests/observer-registry.test.mjs +157 -0
  154. package/tests/orchestrator-cleanup.test.mjs +358 -0
  155. package/tests/orchestrator-command.test.mjs +14 -0
  156. package/tests/orchestrator-persist.test.mjs +375 -0
  157. package/tests/orchestrator-tools.test.mjs +215 -0
  158. package/tests/orchestrator.test.mjs +396 -0
  159. package/tests/package.test.mjs +37 -0
  160. package/tests/pipeline.test.mjs +239 -0
  161. package/tests/planner-classification.test.mjs +81 -0
  162. package/tests/planner-split.test.mjs +67 -0
  163. package/tests/qoder-observer.test.mjs +266 -0
  164. package/tests/reassign-progression.test.mjs +104 -0
  165. package/tests/retry-escalation.test.mjs +120 -0
  166. package/tests/routing.test.mjs +110 -0
  167. package/tests/sqlite-concurrency.test.mjs +178 -0
  168. package/tests/task-global-e2e.test.mjs +63 -0
  169. package/tests/task-global-failed.test.mjs +134 -0
  170. package/tests/telemetry.test.mjs +173 -0
  171. package/tests/test-sync-pi.ps1 +56 -0
  172. package/tests/turn-budget.test.mjs +106 -0
@@ -0,0 +1,55 @@
1
+ # Architecture
2
+
3
+ ## Layers
4
+
5
+ ```
6
+ ┌─────────────────────────────────────────────────────────────┐
7
+ │ common (backend-neutral) │
8
+ │ rules/ skills/ agents/ routing/ context-pack/ lib/ │
9
+ │ mcp/servers.json │
10
+ ├─────────────────────────────────────────────────────────────┤
11
+ │ adapters/<backend-runtime>/ (pi, codex, later: local, …) │
12
+ │ models.json templates sync script runtime-specific glue │
13
+ ├─────────────────────────────────────────────────────────────┤
14
+ │ user environment (~/.pi/agent, ~/.codex) — never owned │
15
+ └─────────────────────────────────────────────────────────────┘
16
+ ```
17
+
18
+ - **rules/** — text policy every agent gets. Two files: `common.md`, `loop-prevention.md`.
19
+ - **skills/** — knowledge + scripts that any runtime with Agent-Skills support can load.
20
+ - **agents/** — role contracts. Frontmatter is pi-subagents compatible; the only kit-specific
21
+ key is `capability`.
22
+ - **routing/** — capability → backend, with fallback and escalation ladders.
23
+ - **context-pack/** — the interchange format between cheap and expensive agents.
24
+ - **lib/** — validators/loaders; plain Node ESM, no dependencies, used by scripts and tests.
25
+ - **adapters/** — everything concrete. pi is the first-class runtime; Codex is just a backend.
26
+
27
+ ## Data flow (target state)
28
+
29
+ 1. User task → parent (pi main session).
30
+ 2. Parent classifies the task into a capability (manual today; a classifier later).
31
+ 3. `scout` (cheap-code) explores and emits a **Context Pack**.
32
+ 4. Parent validates the pack (`lib/context-pack.mjs`) and hands it to `coder` / `visual` /
33
+ `reviewer`, whose capability resolves through routing → adapter model map.
34
+ 5. On failure, escalation ladder moves to the next capability (e.g. cheap-code → strong-code).
35
+
36
+ The orchestrator (`lib/orchestrator/`, `docs/orchestrator.md`) runs steps 2 to 5 as a bounded loop.
37
+ It uses the same agents, routing and adapter invoker. Sub-agent decision requests go through
38
+ `orchestration/decision-policy.json` before the user is involved. A run can be stored in SQLite
39
+ and resumed from a later process; see `docs/orchestrator.md`.
40
+
41
+ Separately, `scripts/reevaluate-models.mjs` is a periodic maintenance task: when provider
42
+ conditions change (free-campaign end, quota/rate-limit change, deprecation, price change,
43
+ new model), it re-scores each capability's binding against `adapters/<x>/model-catalog.json`
44
+ and writes a proposal to `adapters/<x>/out/model-maintenance.proposal.json`. Phase 2 wraps it
45
+ in execution tiers — monitor / evaluate / reconfigure — each selecting a model under a
46
+ free -> cheapest-sufficient -> local-fallback policy (`lib/maintenance-exec.mjs`,
47
+ `adapters/<x>/maintenance-policy.json`), audited in `out/model-maintenance.run.json`.
48
+ Proposals only — applying is a manual `models.local.json` edit + re-resolve.
49
+ See `docs/model-maintenance.md`.
50
+
51
+ ## Windows-native constraints
52
+
53
+ - PowerShell 7 scripts; Junctions (no symlink privilege needed); no WSL.
54
+ - Paths derive from `$env:USERPROFILE`, `$env:PI_CODING_AGENT_DIR`, `$PSScriptRoot`; never a
55
+ fixed user name (enforced by `tests/kit.test.mjs`).
@@ -0,0 +1,44 @@
1
+ # Migration from codex-setting
2
+
3
+ `codex-setting` is frozen: no new features; it continues to work as the Codex installer
4
+ and as the current provider of the `~/.pi/agent` Junctions on this machine.
5
+
6
+ ## Classification of codex-setting files
7
+
8
+ ### Reused (copied into this kit)
9
+ | codex-setting | ludi-agent-kit |
10
+ | --- | --- |
11
+ | `skills/visual-verification/**` (minus `agents/openai.yaml`) | `skills/visual-verification/` (SKILL.md path examples de-Codexed) |
12
+ | `skills/project-management/**` | `skills/project-management/` |
13
+ | `pi/harness/skills/pi-workflow/**` | `skills/pi-workflow/` |
14
+ | `pi/harness/AGENTS.md` | `rules/common.md` (plus a Context-Pack sentence) |
15
+ | `shared/loop-prevention.md` | `rules/loop-prevention.md` |
16
+ | `pi/extensions/index.js` | `adapters/pi/loop-guard/index.js` (policy path updated) |
17
+ | `tests/test-swe-loop-guard.mjs` | `tests/loop-guard.test.mjs` (fixture provider names neutralized) |
18
+ | `skills/visual-verification/agents/openai.yaml` | `adapters/codex/skill-metadata/...` |
19
+ | `LICENSE`, `.gitattributes` | same |
20
+
21
+ ### Left in codex-setting (Codex-specific)
22
+ `scripts/install.ps1`, `update.ps1`, `check.ps1`, `common.ps1`, `tests/test-installation.ps1`,
23
+ `skills/subagent-management/`, `codex/README.md`, `shared/resources.json`
24
+ (`commonPolicyEndHeading`, `~/.codex/AGENTS.md` projection).
25
+
26
+ ### Re-implemented rather than copied
27
+ | codex-setting | reason | kit replacement |
28
+ | --- | --- | --- |
29
+ | `scripts/install-pi.ps1`, `migrate-pi-harness.ps1`, `activate-pi-harness.ps1` | coupled to codex-setting layout, Codex AGENTS projection, PATH/pi-web edits | `adapters/pi/sync-pi.ps1` (dry-run default, no settings/PATH writes) |
30
+ | `scripts/check-pi.ps1/.mjs`, `tests/test-pi-*.{ps1,mjs}` | read `codex-settings/pi.json` state | `tests/test-sync-pi.ps1`; RPC startup check deferred |
31
+ | `pi/agents/{main,coder,vision,reviewer}.md` | hardcoded `model: swe-2-high` | `agents/{scout,coder,visual,reviewer}.md` with `capability` |
32
+ | `pi/README.md`, `HARNESS-DESIGN.md`, `VALIDATION.md` | historical | `docs/` |
33
+
34
+ ## Cut-over on this machine (not performed)
35
+
36
+ 1. `pwsh adapters/pi/sync-pi.ps1` — review `adapters/pi/out/plan.json`.
37
+ 2. `pwsh adapters/pi/sync-pi.ps1 -Apply -BackupConflicts` — re-points skill Junctions,
38
+ writes generated AGENTS.md (old one backed up), adds `agents/ludi-agent-kit` and
39
+ `extensions/ludi-agent-kit`.
40
+ 3. Manually remove the old `agents` and `extensions/codex-settings` Junctions (link only)
41
+ to avoid a duplicate loop guard.
42
+ 4. Restart pi; `pi` → `/reload`; confirm skills appear and the loop guard loads once.
43
+ 5. Rollback: restore `ludi-agent-kit/backup-*/AGENTS.md`, recreate the old Junctions from
44
+ codex-setting (`scripts/install-pi.ps1 -Compact`).
@@ -0,0 +1,401 @@
1
+ # Model maintenance — periodic subagent re-evaluation
2
+
3
+ Provider conditions change: free campaigns end, quotas and rate limits move, models are
4
+ deprecated or removed, prices change, new models appear. `scripts/reevaluate-models.mjs`
5
+ is the periodic maintenance task that re-checks whether each capability's current model
6
+ binding is still rational — and produces a **proposal only**. Nothing is applied
7
+ automatically.
8
+
9
+ ## What it reads
10
+
11
+ | Input | Path | Role |
12
+ | --- | --- | --- |
13
+ | routing | `routing/routing.json` | capabilities, backend tiers, `requires.vision` |
14
+ | agents | `agents/*.md` | capability → agent names (for `affected.agents`) |
15
+ | model bindings | `adapters/<x>/models.json` + `models.local.json` | current provider/model per backend |
16
+ | catalog | `adapters/<x>/model-catalog.json` | model facts: status, cost, contextK, vision, toolUse, quality scores, `location`/`local` power metadata |
17
+ | exec policy | `adapters/<x>/maintenance-policy.json` | Phase 2: requiredQuality per tier, capabilityRequirements (coding/reasoning floors), cost weights, electricity price, escalation thresholds |
18
+ | events | `--events <file>` | what changed (see event types below) |
19
+ | availability | `--check-availability` or `--availability-file` | optional `pi --list-models` probe |
20
+
21
+ ## Free-capacity planning
22
+
23
+ Each maintenance run includes `freeCapacityPlan` even if no provider event arrived.
24
+ For each capability it lists free cloud candidates that meet configured coding/reasoning
25
+ floors, required vision, tool-use and known availability. It reports the suggested
26
+ candidate, campaign cutoff, current primary and existing fallback backends; this is
27
+ **advisory**, not a routing or live settings change. Catalog scores are heuristic:
28
+ validate task success before a binding change. A missing quality requirement produces
29
+ no automatic recommendation. If no free model qualifies, retain the existing route
30
+ or review a paid fallback rather than weakening the capability requirement.
31
+
32
+ `freeUntil` is an exclusive UTC timestamp. At/after the cutoff the catalog's effective
33
+ view uses `postCampaignCost` (or unknown cost), and a maintenance run detects the
34
+ expiry even without an explicit event. Check the provider's actual entitlement and
35
+ billing: catalog dates, especially user-reported campaign dates, are not a guarantee.
36
+ For Devin SWE-2-high, "through 10/10" is interpreted as through 2026-10-10 JST,
37
+ with the exclusive cutoff 2026-10-11 00:00 JST. Confirm year/timezone with Devin
38
+ before relying on that boundary. Qoder's campaign end remains unknown, so verify
39
+ its live free entitlement before using it at scale.
40
+
41
+ ## Event types
42
+
43
+ `free-campaign-ended`, `free-quota-changed`, `rate-limits-changed`, `price-changed`,
44
+ `deprecated`, `removed`, `model-added`. One event file:
45
+
46
+ ```json
47
+ {"version":1,"events":[{"type":"free-campaign-ended","provider":"qoder","model":"Qwen3.8-Flash","asOf":"2026-03-01"}]}
48
+ ```
49
+
50
+ ## What it does
51
+
52
+ For every backend that is the **primary** of at least one capability:
53
+
54
+ 1. Apply events to the catalog entry (`free-campaign-ended` re-prices to
55
+ `postCampaignCost`; `deprecated`/`removed` change status).
56
+ 2. Score current and candidate models with tier-weighted dimensions — cost, coding,
57
+ reasoning, speed, context, tool use (`lib/maintenance.mjs` `TIER_WEIGHTS`: `free`/`low`
58
+ tiers weight cost, `mid`/`high` weight quality). Capability `requires.vision` and
59
+ `toolUse:none` filter the candidate pool.
60
+ 3. Decide:
61
+ - **keep** — best candidate advantage is below the margin (default 8, `--margin`), or
62
+ the candidate data is low-confidence. *A free campaign ending is not by itself a
63
+ reason to switch*: the current model is re-scored with its post-event cost and only
64
+ loses on merit.
65
+ - **propose** — advantage ≥ margin, or a forced migration (`removed` / `deprecated` /
66
+ absent from a provider listing that was successfully probed).
67
+ - **insufficient-data** — no eligible candidate or the current model can't be scored.
68
+ - **skip** — backend unbound or placeholder. Fallback-only backends are reported as
69
+ `not-primary`, not evaluated.
70
+
71
+ ## Information sources and fallback
72
+
73
+ | Source | How | Fallback when unavailable |
74
+ | --- | --- | --- |
75
+ | Model facts (price, context, scores) | `model-catalog.json`, maintained by hand from provider pricing pages / model docs / market snapshots | `scores: null` dims are excluded from scoring; a fully-unknown current model yields `insufficient-data`, never a speculative switch |
76
+ | Provider events | `--events` file written by the operator (or a future fetcher) | no events → availability + catalog status still catch `removed`/`deprecated` |
77
+ | Live availability | `--check-availability` runs `pi --list-models` (read-only) | probe failure → catalog status only; `infoStatus.availability` records which path ran. A provider missing from the listing means *unknown*, never *gone* |
78
+
79
+ ## What a proposal contains
80
+
81
+ Each `changes[]` entry: `changeReason`, `affected` (backend, capabilities, agents),
82
+ `currentModel`, `proposedModel`, `expectedCostImpact`, `expectedQualityImpact`,
83
+ `scores` (current/proposed/delta/margin), `confidence`, and `rollback` — the previous
84
+ binding embedded plus the manual steps to restore it.
85
+
86
+ ## Applying and rolling back
87
+
88
+ The task writes only `adapters/<x>/out/model-maintenance.proposal.json`. To apply:
89
+ edit `adapters/<x>/models.local.json` per `proposedModel`, re-run
90
+ `scripts/resolve-capabilities.mjs`, then merge `out/settings.proposal.json` into
91
+ `~/.pi/agent/settings.json` yourself. Rollback is the same flow using
92
+ `rollback.previousBinding`. It never writes `settings.json`, `models.json`,
93
+ `models.local.json`, `routing.json`, or anything under `~/.pi`.
94
+
95
+ ## Phase 2 — execution tiers (monitor / evaluate / reconfigure)
96
+
97
+ The maintenance run itself has a cost hierarchy: the AI that watches and judges model
98
+ config is chosen under a **free -> cheapest-sufficient -> local fallback** policy, and
99
+ only escalates to a premium tier when the decision warrants it. The run is a dry-run
100
+ decision layer: `runMaintenancePlan` (`lib/maintenance-exec.mjs`) selects which model
101
+ each tier *would* run on and records why — no model is invoked, no config is written.
102
+
103
+ ```
104
+ monitor cheapest watcher: diffs events/catalog/availability ->
105
+ { changed, reasons, affectedModels, severity, escalationRequired }
106
+ | changed
107
+ evaluate cheapest-sufficient judgement: interprets evaluateMaintenance() ->
108
+ keep-inside-margin | proposal
109
+ | complex / low-confidence / large blast radius
110
+ reconfigure premium tier (a strong coding model at that point in time — never a
111
+ hardcoded name): multi-capability or structural re-optimisation
112
+ |
113
+ proposal still only out/model-maintenance.proposal.json
114
+ ```
115
+
116
+ ### cheapest-sufficient selection
117
+
118
+ Per tier, `selectTierModel` computes for every catalog model:
119
+
120
+ ```
121
+ quality = tier-weighted 0-100 score (coding/reasoning/speed/context/toolUse)
122
+ eligible = usable status AND toolUse != none AND availability != 'unavailable'
123
+ AND quality >= requiredQuality[tier]
124
+ effectiveCostUsd/run = api($/1M blended) + electricity + speedPenalty
125
+ selected = free cloud eligible? -> cheapest of those
126
+ else cheapest eligible cloud
127
+ else cheapest eligible local (local fallback)
128
+ ```
129
+
130
+ Free is preferred **only when it meets the quality bar** — a free model below
131
+ `requiredQuality` is rejected and selection proceeds to cheapest-sufficient.
132
+ Unknown cost components fall back to the cheapest *known* value in the pool, so an
133
+ unpriced model is never treated as free. `availability: unknown` (probe absent or
134
+ provider unseen) stays eligible; only `unavailable` (absent from a successful listing)
135
+ is excluded.
136
+
137
+ ### Escalation conditions (evaluate -> reconfigure)
138
+
139
+ Any one of: proposed changes span >= `minCapabilities` (2) capabilities or >=
140
+ `minAgents` (3) agents; largest quality swing >= `minQualitySwing` (15 points); a
141
+ winning margin below `maxScoreDelta` (4 — a hard call); low-confidence or
142
+ insufficient-data decisions; a structural event (`removed`/`deprecated`); or monitor
143
+ severity `high`. The escalation is recorded on the run and on every proposed change as
144
+ `escalationReason`, `sourceTier`, `targetTier`, `affectedCapabilities`,
145
+ `estimatedDecisionCostUsd`.
146
+
147
+ ### Local fallback and electricity cost
148
+
149
+ Catalog entries with `location: "local"` may carry `local: { powerWatts, taskMinutes }`.
150
+ Estimated run cost: `(powerWatts/1000) * (taskMinutes/60) * electricityPricePerKwh`
151
+ (`maintenance-policy.json`, default $0.30/kWh). Local entries join the same scoring —
152
+ if a cloud API run is cheaper than the electricity estimate, the cloud model wins
153
+ (Case E in `tests/maintenance-exec.test.mjs`).
154
+
155
+ ### Run report
156
+
157
+ Every run writes `adapters/<x>/out/model-maintenance.run.json` (gitignored): per-tier
158
+ `selected` model, `selectionPath` (`free-cloud` / `cheapest-sufficient-cloud` /
159
+ `local-fallback`), `selectionReason`, full `candidates` list with `rejectedReasons`,
160
+ `estimatedDecisionCostUsd`, `fallbackOccurred`, the monitor output, the escalation
161
+ record, and the Phase 1 proposal when produced. No credentials are ever stored.
162
+
163
+ Policy defaults live in `lib/maintenance-exec.mjs` (`DEFAULT_POLICY`); the adapter
164
+ overrides them in `adapters/<x>/maintenance-policy.json` (`--policy` to point elsewhere).
165
+
166
+ ## Phase 3 — real invocation (`--live`)
167
+
168
+ `node scripts/reevaluate-models.mjs --live` runs the same tiered pipeline but actually
169
+ calls the selected models through the pi adapter invoker (`adapters/pi/lib/invoke.mjs`:
170
+ `pi -p --model <provider/id[:thinking]> --no-tools --no-session --no-approve`). No new
171
+ execution substrate — model ids come from the same catalog/selection path.
172
+
173
+ ### Deterministic authority boundary
174
+
175
+ The engine decides: eligible models, `requiredQuality`, cost, availability, escalation
176
+ conditions, and the keep/propose outcome. The LLM only interprets and annotates
177
+ (`reasoningSummary`, `proposalNotes`, `recommendedActions`). `recommendedModels`
178
+ outside the catalog are stripped and recorded as `rejectedRecommendations`. If a tier's
179
+ output fails schema validation twice (one retry), the run continues on the
180
+ deterministic result alone (`degradedToDeterministic`).
181
+
182
+ ### Tier prompts and schemas
183
+
184
+ Each tier gets a fixed prompt (`buildTierPrompt`) embedding the deterministic facts and
185
+ a required JSON shape (`TIER_SCHEMAS`):
186
+
187
+ - **monitor** — organise diffs; `decision: changed|no-change`, `severity`,
188
+ `evaluateNeeded`. Cannot propose routing changes.
189
+ - **evaluate** — judge the deterministic result; `decision: keep|propose|insufficient-data`,
190
+ `confidence`, `proposalNotes`. Confidence below `escalation.minEvaluateConfidence`
191
+ (0.5) escalates.
192
+ - **reconfigure** — re-review the whole picture for structural/multi-capability
193
+ changes; adds `routingNotes`. Output is advisory only.
194
+
195
+ ### Cost per run
196
+
197
+ `estimatedCostPerRun` normalises cloud and local to USD/run:
198
+ `api = (inTokens x $/1M-in + outTokens x $/1M-out)` from `taskProfiles[tier]`
199
+ (`estimatedInputTokens`/`estimatedOutputTokens`/`estimatedTaskMinutes`); local uses
200
+ `powerWatts x taskMinutes x electricityPricePerKwh`. Plus a small speed penalty.
201
+
202
+ ### Failure and fallback
203
+
204
+ On invoke failure / quota / timeout / two bad outputs, the runner walks the
205
+ deterministic `ordered` candidate chain (free-cloud, then sufficient pool by cost).
206
+ Premium models (`premium: true` in the catalog) only enter the chain at the
207
+ reconfigure tier. Audit lands in `out/model-maintenance.run.json` under `invocations[]`:
208
+ model, ok/failureClass, schemaValid, latencyMs, tokens/actualCost when reported,
209
+ estimatedCostUsd, fallback chain. Prompts and credentials are never stored.
210
+
211
+ ## Phase 4 — observation layer (`scripts/observe-models.mjs`)
212
+
213
+ External provider conditions are collected as **normalized observations**, stored,
214
+ deduped, diffed against the catalog, and turned into a *proposal* — never applied.
215
+ The layer is separate from the core engine (`lib/observe/`); the maintenance logic
216
+ stays free of provider/web specifics.
217
+
218
+ ```
219
+ external sources (manual / pi --list-models / fixture announcements)
220
+ -> source adapters (lib/observe/sources.mjs)
221
+ -> normalized observations (validate + hash)
222
+ -> out/model-observations.jsonl (dedupe: same hash = duplicate; older = stale)
223
+ -> resolve conflicts (newer > trust > confidence; equal-trust major diff = conflict)
224
+ -> out/catalog-diff.json + out/model-catalog.proposal.json
225
+ -> --preview-maintenance: hypothetical catalog -> Phase 1-3 dry-run
226
+ ```
227
+
228
+ ### Observation schema
229
+
230
+ `{ provider, model, observedAt, source:{type:manual|api|web|cli, trust, url, label},
231
+ changes:{availability,status,free,freeUntil,inputPricePer1M,outputPricePer1M,
232
+ contextK,toolUse,vision}, confidence:0-1, evidence:[] }`. `null` = not observed —
233
+ distinct from `false`/`0`.
234
+
235
+ ### Trust order
236
+
237
+ `manual_verified > provider_api > provider_web > pi_cli > third_party > unknown`.
238
+ Conflicts resolve by newer timestamp, then trust, then confidence; an equal-trust
239
+ disagreement on a major field (status/free/prices) is recorded as `conflict`, never
240
+ auto-resolved.
241
+
242
+ ### Catalog proposal rules
243
+
244
+ `additions` (new model observed active), `updates` (field changes), `deprecations`
245
+ (status removed/deprecated — requires >=2 independent sources or one high-trust;
246
+ a listing absence alone is never removal), `conflicts`, `ignored`, `freshness`.
247
+ `applyProposalToCatalog` builds an in-memory hypothetical catalog for the preview.
248
+
249
+ ### CLI
250
+
251
+ ```powershell
252
+ node scripts/observe-models.mjs --source manual --input obs.json
253
+ node scripts/observe-models.mjs --source fixture --input announcements.json
254
+ node scripts/observe-models.mjs --check-pi
255
+ node scripts/observe-models.mjs --check-qoder # qoder-models-cache.json priceFactor -> free observations
256
+ node scripts/observe-models.mjs --check pi-cli,qoder-cache # generic observer list (same ids)
257
+ node scripts/observe-models.mjs --preview-maintenance
258
+ ```
259
+
260
+ ### Observer registry
261
+
262
+ Live probes are **observers** in `lib/observe/observers.mjs` — a common
263
+ `async run(context) -> { observations, probeFailed, metadata }` interface behind a
264
+ registry (`OBSERVERS`). `--check-pi`/`--check-qoder` map to `pi-cli`/`qoder-cache`;
265
+ `--check <csv>` accepts any ids. `resolveRequestedObservers` produces a stable,
266
+ deduped id list (unknown ids are an explicit error, never silently skipped);
267
+ `runObservers` executes each once, in order, isolating failures (one observer's
268
+ `probeFailed` never blocks the others; per-source results are audited on the run
269
+ record). Adding a provider = one registry entry — no CLI/job branching.
270
+
271
+ ### Fixture / test boundary
272
+
273
+ Observations carry `environment: 'production' | 'test'` and `sourceFixture`. The
274
+ `fixture` source (`--source fixture`, `tests/fixtures/**` replays) tags its output
275
+ `test`. `productionObservations()` filters the store before `diffCatalog`/
276
+ `buildCatalogProposal` in both `observe-models.mjs` and `job.mjs`, so a fixture
277
+ replay is stored for audit but **never** drives a real catalog/routing proposal.
278
+ Anything without a marker counts as production (back-compat).
279
+
280
+ ### Qoder provider-metadata observer (`--check-qoder`)
281
+
282
+ Reads `~/.pi/agent/qoder-models-cache.json` (schema v2: `{version, updatedAt,
283
+ models:[{id, priceFactor, ...}]}`) — the pi-maintained, provider-derived local cache —
284
+ **read-only**. `priceFactor` is the provider's subscription cost multiplier:
285
+ `0` = currently free, `>0` = paid. A missing file, unparseable JSON, absent entry,
286
+ or absent/invalid `priceFactor` yields *no observation* (unknown) — never a guessed
287
+ "not free". Trust tier `provider_local_cache`.
288
+
289
+ Transition detection compares against `out/qoder-observer-state.json` (gitignored
290
+ snapshot the job owns): `0 -> non-zero` emits `free:false` (campaign ended),
291
+ `non-zero -> 0` emits `free:true` (campaign started), same value emits nothing.
292
+ The emitted `free` field flows through the normal pipeline — catalog diff ->
293
+ proposal -> hypothetical catalog -> `free-campaign-ended|started` events ->
294
+ maintenance re-evaluation. A free end alone never forces a routing change;
295
+ `postCampaignCost: null` becomes `costUnknown` (not $0, not free) and is surfaced
296
+ in the proposal for human review. `--qoder-cache <path>` overrides the cache
297
+ location (tests/fixtures).
298
+
299
+ ## Phase 5 — scheduled job + notifications (`scripts/model-maintenance-job.mjs`)
300
+
301
+ `model-maintenance-job.mjs` wraps observe -> dedupe -> diff -> meaningful-change gate
302
+ -> proposal -> preview -> optional `--live` tiers -> notification. Quiet by default:
303
+ only human-worthy changes notify. Exit code `0` quiet, `2` meaningful change pending
304
+ review, `1` error.
305
+
306
+ ### Meaningful change & severity (deterministic, never LLM)
307
+
308
+ Quiet when: no observations / duplicates-only / stale-only / catalog unchanged /
309
+ no routing impact / probe-failed-only / unknown-only. Otherwise:
310
+
311
+ - **info** — new model, context/capability additions, no routing impact
312
+ - **action** — free-model end, cheapest-sufficient shift, preview binding change,
313
+ all-tier-candidates-failed
314
+ - **urgent** — current model removed/deprecated/unavailable, fallback to local,
315
+ no eligible candidate for an *unusable* backend, multi-capability impact
316
+
317
+ ### Notification
318
+
319
+ Canonical payload (`severity,title,summary,changes,routingImpact,recommendedNextStep,
320
+ requiresApproval:true,proposalPath,previewPath,runId,observedAt`). Sinks: stdout,
321
+ `out/model-maintenance.notification.json`, and an opt-in `--notify-command` that
322
+ receives the JSON on stdin (audited; only runs when explicitly given). Dedupe:
323
+ `out/model-maintenance.state.json` stores `lastNotificationHash`/`lastSeverity`;
324
+ identical content is not resent, a severity rise re-notifies.
325
+
326
+ ### Concurrency & budget
327
+
328
+ Single-run lock (`out/model-maintenance.lock`, stale after 30 min, auto-recovered),
329
+ atomic artifact writes (tmp+rename), `runId`/`startedAt`/`completedAt` in
330
+ `out/model-maintenance.lastrun.json`. Budget (`maintenance-policy.json` `budget`):
331
+ `maxEstimatedCostPerRunUsd`, `maxPremiumInvocationsPerRun`, `maxTotalInvocationsPerRun`
332
+ — over-budget skips invocations, continues deterministically, sets `budgetLimited`.
333
+
334
+ ## Phase 6 — shadow mode + calibration (`scripts/report-model-maintenance.mjs`)
335
+
336
+ `--shadow` runs the normal pipeline but suppresses the external `--notify-command`
337
+ (stdout + file sinks still fire; `--shadow --shadow-notify` opts the command back in).
338
+ Every run appends a compact record to `out/telemetry-runs.jsonl` and folds into
339
+ `out/telemetry.json` via `lib/telemetry.mjs`.
340
+
341
+ ### Telemetry
342
+
343
+ Aggregates: run/quiet/meaningful/notification/dedupe counts, severity distribution,
344
+ per-tier invocations, fallbacks (incl. local), premium invocations, budget-limited,
345
+ degraded-to-deterministic, observer failures, conflicts, duplicate/stale
346
+ observations, and cost totals (api / local-electricity / total, per-run, per
347
+ meaningful event, per notification). Per-decision records keep selected model,
348
+ tier, estimatedCostPerRun, quality vs requiredQuality margin, alternatives and
349
+ rejection reasons — no prompts or credentials.
350
+
351
+ ### Report
352
+
353
+ `node scripts/report-model-maintenance.mjs [--days N] [--json] [--compact]` prints
354
+ Activity / Cost / Routing / Quality / Noise / Escalation sections, deterministic
355
+ warnings (too-noisy, too-expensive, too-many-premium-escalations,
356
+ too-many-fallbacks, quality-margin-too-small), counterfactual comparisons of the
357
+ selected model vs the runners-up it beat, and a policy calibration proposal.
358
+
359
+ ### Calibration proposal + guards
360
+
361
+ `out/maintenance-policy.calibration.proposal.json` — each entry has
362
+ currentValue/proposedValue/evidence/expectedEffect/confidence. Emitted only when
363
+ `calibration.minRuns` (20) and `calibration.minMeaningfulEvents` (3) are met;
364
+ otherwise `insufficient-observation-data`. Never applied automatically.
365
+
366
+ ### Retention
367
+
368
+ `--compact --days 30` folds raw run lines older than the window into a
369
+ `_compactedSummary` record; recent lines are kept, corrupt lines are never dropped,
370
+ and the rewrite is atomic (tmp+rename) so a failure preserves the original file.
371
+
372
+ ## Scheduling
373
+
374
+ Run it when a provider announcement lands, or periodically:
375
+
376
+ Recommended cadence: every 6-12 hours (price/model conditions don't move per-minute;
377
+ avoid over-polling). One run at startup is reasonable. Overlap is prevented by the
378
+ lock file.
379
+
380
+ ```powershell
381
+ node scripts/model-maintenance-job.mjs --check-pi --check-qoder --shadow # recommended: availability + Qoder priceFactor, observe-only
382
+ # tests/fixtures/** replays write test-tagged observations; they never reach the production diff
383
+ node scripts/model-maintenance-job.mjs --check-pi # availability only
384
+ node scripts/model-maintenance-job.mjs --check-qoder # Qoder priceFactor only
385
+ node scripts/model-maintenance-job.mjs --check-pi --live # + real tier invocations when needed
386
+ node scripts/model-maintenance-job.mjs --notify-command "node send.js" # explicit external sink
387
+ # Windows Task Scheduler example (NOT registered by the kit):
388
+ # schtasks /create /tn ludi-model-maintenance /sc hourly /mo 6 ^
389
+ # /tr "node <kit>\scripts\model-maintenance-job.mjs --check-pi"
390
+ # exit code 2 = a notification is waiting for human review.
391
+
392
+ # Recommended initial rollout (Phase 6): shadow mode, no external notify,
393
+ # review the calibration report after 7 days.
394
+ # schtasks /create /tn ludi-model-maintenance /sc hourly /mo 6 ^
395
+ # /tr "node <kit>\scripts\model-maintenance-job.mjs --check-pi --shadow"
396
+ # node <kit>\scripts\report-model-maintenance.mjs --days 7
397
+ ```
398
+
399
+ The output file is gitignored (`adapters/pi/out/`); copy `changes[]` into an issue or
400
+ notes if you want history. Fixture for the Qoder/Qwen3.8-Flash free-campaign-end
401
+ scenario: `tests/fixtures/maintenance/`, exercised by `tests/maintenance.test.mjs`.
@@ -0,0 +1,155 @@
1
+ # Orchestrator (Phase 1)
2
+
3
+ One high-level request in, one integrated report out. The orchestrator plans, routes,
4
+ delegates, evaluates and decides; it never implements. Its main job is to cut down how
5
+ often the user gets asked something.
6
+
7
+ ```
8
+ node scripts/orchestrate.mjs --dry-run "DOLL v2 Phase 2を進める" # plan + routing only
9
+ node scripts/orchestrate.mjs --repo <dir> "Fix the failing test" # run (agents read-only)
10
+ node scripts/orchestrate.mjs --repo <dir> --apply "..." # coder tasks via scout->coder pipeline (writes)
11
+ [--planner rules|model] [--policy <file>] [--out <dir>] [--json] [--trace]
12
+ ```
13
+
14
+ ## Response language
15
+
16
+ All agents default to **Japanese** for user-facing output (summaries, reports, decisions,
17
+ status messages). The policy lives once in `lib/language-policy.mjs` and is appended to
18
+ every agent's system prompt by `loadAgents` — never duplicated into `agents/*.md`, never
19
+ per-provider. Schema keys/enums (`status`, `completed`, Context Pack `##` headings, JSON
20
+ field names) stay in English; only natural-language values are Japanese. An explicit user
21
+ language request ("英語で", "in English") overrides the default. `formatReport` already
22
+ renders 状態/完了/自動判断/未解決/ユーザー判断が必要 in Japanese.
23
+
24
+ ## Modules (`lib/orchestrator/`)
25
+
26
+ | Module | Responsibility |
27
+ | --- | --- |
28
+ | `planner.mjs` | request -> task specs. `rules` (deterministic keyword template, default) or `model` (orchestrator agent returns JSON; invalid output falls back to rules). `validatePlan` checks ids, deps, cycles, agents, capabilities, `max_tasks`. |
29
+ | `task-store.mjs` | `createMemoryTaskStore` (Phase 1) plus `runnableTasks`, `strandedTasks`, `findCycle`. The persistent adapter is `store.mjs`. |
30
+ | `store.mjs` | SQLite run, task, decision, decision-memory, backend-health and trace store. |
31
+ | `health.mjs` | Classifies usage-limit and rate-limit failures and skips a backend until its TTL. |
32
+ | `router.mjs` | task -> agent -> capability through `agents/*.md` + `routing.json`; models via `lib/resolve.mjs`. Retries escalate along `routing.escalation.ladders`. |
33
+ | `runner.mjs` | AgentRunner adapter: `withEscalation` + adapter `invoke` (pi CLI), or `runPipeline` for `--apply`. Appends the structured-result contract; agent files are unchanged. |
34
+ | `evaluator.mjs` | Success only with status `completed`, a summary, required outputs present and every acceptance criterion `A<n>` met with evidence. |
35
+ | `escalation.mjs` | Escalation gate (below). |
36
+ | `policy.mjs` | Defaults <- `orchestration/decision-policy.json` <- `decision-policy.local.json` / `--policy`. |
37
+ | `orchestrator.mjs` | The loop, `dryRun`, `formatPlan`, `formatReport`. |
38
+ | `api.mjs` | Start, resume, list, show and answer. CLI and the pi extension both call this. |
39
+
40
+ ## Loop
41
+
42
+ Each round: take runnable tasks (pending, all dependencies completed), run up to
43
+ `max_parallel_tasks` of them, then evaluate each result.
44
+ - **success**: the task completes, and any `newTasks` it reports are added (bounded by `max_tasks`).
45
+ If a review reports blocking issues, the work is sent back to the implementer and a
46
+ re-review is added (`max_rework_cycles`).
47
+ - **failure**: the retry strategy depends on the failure class (`failures.mjs`):
48
+ - **Protocol-quality failures** (`MALFORMED_RESULT`, `EMPTY_RESPONSE`, and turn-limit
49
+ `TIMEOUT` with no tool progress) mean the model could not honour the contract — the
50
+ SAME attempt advances to the next routing candidate instead of retrying that model.
51
+ When the current capability has no untried candidate left, the task escalates to the
52
+ next capability on the ladder (e.g. cheap-code -> strong-code).
53
+ - **Recoverable failures** (`TEST_FAILURE`, `TOOL_FAILURE`, validation `UNKNOWN`, a
54
+ turn-limit `TIMEOUT` that DID make tool progress) retry the same model with feedback.
55
+ - Two-layer tried history. `task.attemptedModels` is the CAPABILITY-LOCAL tried
56
+ list (reset on escalation so the new capability's candidates re-resolve).
57
+ `task.taskGlobalFailedModels` is the TASK-GLOBAL list: a model that hit a
58
+ protocol-quality failure (malformed / empty / no-progress turn-limit) is skipped
59
+ for the WHOLE task even after escalation — so cheap-code's failed Qwen is never
60
+ re-invoked on strong-code. Recoverable failures (tool/test/validation) do NOT
61
+ mark a model task-global.
62
+ - Attempt budget: `model_attempts_per_task` caps candidates per attempt;
63
+ `max_total_attempts_per_task` caps total REAL invocations. Only an actual
64
+ model/provider invocation counts — health-TTL skips, unavailable candidates,
65
+ already-tried skips and enumeration do NOT consume budget. Each result carries
66
+ `counters = { candidatesConsidered, candidatesSkipped, invocationsStarted }` so
67
+ the budget display shows real invocations (e.g. `3/4`), not candidates seen.
68
+ Once retries (or the model-attempt budget) run out the task is marked failed.
69
+
70
+ ### Subagent execution budget
71
+
72
+ Turn limits are per-role and per-complexity, not a single global value
73
+ (`agent_runtime.turn_budgets`). `classifyTaskComplexity` rates a task
74
+ simple/normal/heavy/repo-history-heavy from its goal; `initialTurnBudget` resolves
75
+ the starting turn cap. When a subagent reaches that cap, a deterministic
76
+ `progressScore` (tool calls + unique files + commands − repeats) decides whether
77
+ to grant a bounded extension (`extension_turns` × `max_extensions`, capped by
78
+ `absolute_max_turns`). A no-progress agent stops immediately as
79
+ `NO_PROGRESS_TIMEOUT` (task-global model failure); a progressing agent that still
80
+ exceeds the absolute cap is `PROGRESS_TIMEOUT` — recoverable, NOT a model-quality
81
+ failure, so it is not added to `taskGlobalFailedModels`. Extension happens inside
82
+ one model invocation and never consumes `max_total_attempts_per_task`.
83
+
84
+ The planner also splits multi-concern investigations (e.g. commit-history +
85
+ UI-implementation + synthesis) into focused sub-tasks so no single scout is
86
+ overloaded — task sizing comes before turn budget.
87
+ - **blocked**: the agent needs a decision, so each decision goes through the escalation gate.
88
+
89
+ Protocol reliability is audited per provider/model in the `protocol_stats` table
90
+ (malformed / empty / turn_limit / structured_ok counts). This is telemetry only — it is
91
+ NOT fed back into routing scores.
92
+
93
+ The loop stops when no task is runnable or `max_rounds` is reached. Tasks whose
94
+ dependency failed or is blocked become `blocked`. The full event log is kept in
95
+ `result.trace`, and the CLI writes it to `<out>/orchestration-trace.json`.
96
+
97
+ ## Escalation gate
98
+
99
+ 1. **Hard gate.** A decision flagged `external_publish`, `destructive_action`, `production_risk`,
100
+ `high_cost`, `project_cancellation`, `major_direction_change` or `user_value_judgement` goes to
101
+ the user when that flag is enabled in policy. Flags come from the agent, from numbers
102
+ (`costUsd >= high_cost_threshold_usd`, `estimatedWeeks >= major_direction_change_weeks`), or
103
+ from policy keywords. This step is first, so a remembered answer cannot authorize it.
104
+ 2. **Current run.** Reuse a decision already made in this run with the same key.
105
+ 3. **Persistent decision memory.** Reuse an explicit user answer (or another saved rule) from an
106
+ earlier run when it is still in scope and not expired. Automatic choices are not saved.
107
+ 4. **Single option.** Take the only viable option.
108
+ 5. **Reversible.** Choose the best-scoring option (`reversible_decision: auto`).
109
+ 6. **Low risk / low cost.** Choose one (`low_risk_decision: auto`).
110
+ 7. **Project policy.** Prefer existing assets, maintainability and reversibility, but only
111
+ when this gives a unique winner.
112
+ 8. **Small experiment.** If the estimate is within `poc.prefer_if_estimated_hours_lte` hours,
113
+ add an experiment task and resume the blocked task afterwards. Each decision gets at most one experiment.
114
+ 9. **Otherwise** escalate. With a store, the run becomes `waiting_for_user` and the decision is
115
+ kept until `orchestrate.mjs --resume <run> --answer <decision> "<text>"`.
116
+
117
+ ## Persistent runs (Phase 2)
118
+
119
+ State lives in SQLite (`node:sqlite`), default `.orchestration/state.db`, override with `--store`
120
+ or `LUDI_ORCHESTRATION_STORE`. SQL stays in `lib/orchestrator/store.mjs`. `createMemoryTaskStore`
121
+ is the Phase 1 store; `openStore().openTaskStore(runId)` is the persistent one. The in-memory
122
+ loop is unchanged when no session is passed.
123
+
124
+ ```
125
+ node scripts/orchestrate.mjs --list
126
+ node scripts/orchestrate.mjs --decisions
127
+ node scripts/orchestrate.mjs --show <run-id>
128
+ node scripts/orchestrate.mjs --resume <run-id> --answer <decision-id> "use the existing approach"
129
+ ```
130
+
131
+ Resume does not rerun `completed` tasks, does not reset attempts, rework cycles, rounds or the
132
+ task cap, and uses the policy snapshot stored with the run. A task left `running` by a crash is
133
+ put back to `pending`. A backend that returns `usage limit has been reached` is skipped for the
134
+ rest of the run and, until `backend_health.usage_exhausted_ttl_hours`, for later runs. Rate limits
135
+ stay on the run that saw them. There is no permanent blacklist.
136
+
137
+ pi loads `adapters/pi/orchestrator-ext` as `extensions/ludi-orchestrator` (`/orchestrate`, tool
138
+ `ludi_orchestrate`). The programmatic boundary is `lib/orchestrator/api.mjs`.
139
+
140
+ ## Tool-capable children (Phase 3)
141
+
142
+ `execution: subagent` runs `pi --mode json -p` with `--tools` limited by `access` (filesystem, shell, git, network). Shell is `ludi_exec` in `adapters/pi/shell-gate`, which refuses push, publish, deploy, reset, and recursive delete before the process runs. `--apply` still uses the old pipeline. With no subagent launcher, `subagent` falls back to the one-shot invoker. A child id, tool count, and turn count are stored on the run trace. Runtime, tool-call, and turn caps are `agent_runtime` in the decision policy. Only `MODEL_FAILURE` and `BACKEND_LIMIT` move up the routing ladder; a failing test retries the same capability with the previous attempt in the contract.
143
+
144
+ Dry-run prints mode, workspace, tools, model, dependencies, and acceptance, and does not start pi.
145
+
146
+ ## Phase 4 gaps (Asana -> persistent orchestrator -> tool-capable agents -> Asana)
147
+
148
+ - An `ExternalProjectStore` implementation for Asana that maps tasks, sections and comments, is
149
+ idempotent, and runs dry-run first. The run store stays separate from that project store.
150
+ - An intake adapter that turns an Asana task into a request plus acceptance criteria.
151
+ - A direct pi-subagents launcher. The current tool-capable child path uses `pi --mode json -p`;
152
+ `createPiInvoker` remains a synchronous fallback for one-shot and model-planning calls.
153
+ - Parallel execution for one-shot invocations. Tool-capable children use asynchronous `spawn`,
154
+ while the one-shot invoker still uses `spawnSync`.
155
+ - A reviewer severity schema and an acceptance rubric shared by all agents.