@ijfw/memory-server 1.4.4 → 1.5.1
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/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Subagent Checkpoint Contract — v1.5.0 S1
|
|
2
|
+
|
|
3
|
+
**Status:** Frozen. Wave 11-A1 wire-up.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Across IJFW v1.4.4 Wave 10 + v1.5.0 research dispatch, **8 of 13 subagents (62%) truncated mid-flow** at the Claude Code harness cap (~20 tool uses or ~60s wall, whichever first). Truncation manifested as silent stop without a Status: report — orchestrator had to read partial worktree state and finish by hand. The checkpoint contract closes this gap by giving the orchestrator a structured record of "what the agent was doing when it died" so it can resume from the last known good state instead of redispatching from scratch.
|
|
8
|
+
|
|
9
|
+
## The CLI
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
ijfw checkpoint <waveId> <subId> <jsonPayload>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `<waveId>` and `<subId>` MUST match `[A-Za-z0-9_-]{1,64}` (no path traversal).
|
|
16
|
+
- `<jsonPayload>` is a JSON object string. Wrapping fields (`schema_version`, `wave_id`, `sub_id`, `ts`) are added by the CLI; do not include them yourself.
|
|
17
|
+
- The CLI atomically writes via `withFsLock` to `.ijfw/wave-<waveId>/subagent-<subId>.checkpoint.json`. Concurrent calls from the same subId serialise.
|
|
18
|
+
|
|
19
|
+
## What to put in the payload
|
|
20
|
+
|
|
21
|
+
Recommended fields (free-form — the schema accepts arbitrary keys, but these are what the orchestrator looks for on resume):
|
|
22
|
+
|
|
23
|
+
| Field | Type | Purpose |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `tool_use_count` | integer | How many tool calls the agent has made. Helps the orchestrator estimate cap proximity. |
|
|
26
|
+
| `last_action` | string ≤200 chars | Human-readable description of what the agent just finished. |
|
|
27
|
+
| `files_created` | string[] | Paths the agent has created so far (relative to project root). |
|
|
28
|
+
| `files_modified` | string[] | Paths the agent has modified so far. |
|
|
29
|
+
| `commits_made` | string[] | SHA list of commits the agent has landed. |
|
|
30
|
+
| `next_step` | string ≤200 chars | What the agent intends to do next. **The orchestrator's resume protocol picks up here.** |
|
|
31
|
+
| `blockers` | string (optional) | Set only if the agent paused due to ambiguity or environment issue. Triggers `NEEDS_CONTEXT` handling. |
|
|
32
|
+
|
|
33
|
+
## Cadence
|
|
34
|
+
|
|
35
|
+
**Checkpoint every substantive tool use OR every 30 seconds, whichever first.** Do not checkpoint after pure read-only tool calls — that just burns your tool budget. Examples of "substantive":
|
|
36
|
+
|
|
37
|
+
- After a Write that produced a non-trivial file.
|
|
38
|
+
- After an Edit that changed code semantics.
|
|
39
|
+
- After a Bash that ran tests, built artifacts, or made commits.
|
|
40
|
+
- After a substantive piece of analysis you don't want to repeat.
|
|
41
|
+
|
|
42
|
+
Examples of "not substantive" (skip the checkpoint):
|
|
43
|
+
|
|
44
|
+
- Read of a small file you already had context for.
|
|
45
|
+
- Glob/Grep that returned nothing surprising.
|
|
46
|
+
|
|
47
|
+
## Size cap
|
|
48
|
+
|
|
49
|
+
Serialised payload (after the CLI adds wrapping fields) must be ≤ **4 KB**. The CLI rejects larger payloads with a clear error. Keep `last_action` and `next_step` under 200 chars each; trim file lists if they grow past ~20 entries.
|
|
50
|
+
|
|
51
|
+
## How orchestrator detects + resumes truncation
|
|
52
|
+
|
|
53
|
+
1. After the agent returns, the orchestrator inspects its message for a `Status: <VALUE>` line per the v1.4.4 status protocol.
|
|
54
|
+
2. If no Status line, the orchestrator treats the agent as truncated.
|
|
55
|
+
3. It calls `listOrphanedSubagents(waveId, projectRoot)` to find subagents with a checkpoint file but no completion marker in STATE.md.
|
|
56
|
+
4. For each orphan, it calls `readLastCheckpoint(waveId, subId, projectRoot)`.
|
|
57
|
+
5. It re-dispatches the subagent with a prepended context block:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
PRIOR CHECKPOINT (from your last truncated run):
|
|
61
|
+
- Tool uses: <tool_use_count>
|
|
62
|
+
- Last action: <last_action>
|
|
63
|
+
- Files created: <files_created>
|
|
64
|
+
- Files modified: <files_modified>
|
|
65
|
+
- Commits made: <commits_made>
|
|
66
|
+
- Intended next step: <next_step>
|
|
67
|
+
- Blockers (if any): <blockers>
|
|
68
|
+
|
|
69
|
+
RESUME FROM `next_step`. Do NOT restart from scratch. Re-verify your prior commits are intact, then continue.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
6. The resumed agent verifies its prior commits + continues. The orchestrator confirms by checking that the next commit touches files in the checkpoint's modified list (or the agent must explain divergence in the commit message).
|
|
73
|
+
|
|
74
|
+
## Backward compatibility
|
|
75
|
+
|
|
76
|
+
Subagents that don't call `ijfw checkpoint` still work — they just have the same truncation-recovery cost as v1.4.4 (orchestrator-side hand-finishing). The contract is opt-in for the implementer; opt-in for the orchestrator's resume protocol.
|
|
77
|
+
|
|
78
|
+
## Real-world evidence
|
|
79
|
+
|
|
80
|
+
| Dispatch | Cap hit at | Outcome |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| v1.4.4 W10-A2 | 19 tool uses / 3:00 | Orchestrator finished in-place |
|
|
83
|
+
| v1.4.4 W10-A3 | 28 / 3:30 | Orchestrator finished in-place |
|
|
84
|
+
| v1.4.4 W10-B2 | 10 / 0:42 | Orchestrator finished in-place |
|
|
85
|
+
| v1.5.0 R1 (research) | 25 / 43s | Lost work — no commit |
|
|
86
|
+
| v1.5.0 R2-first | 33 / 587s | Lost work — empty branch |
|
|
87
|
+
| v1.5.0 R4-first | 55 / 167s | Lost work — partial file recovered |
|
|
88
|
+
| v1.5.0 R1-redo | 19 / 58s | Lost work — even with anti-truncation prompt |
|
|
89
|
+
| v1.5.0 R4-redo | 17 / 49s | Lost work — even with anti-truncation prompt |
|
|
90
|
+
| v1.5.0 W11-A0 | 14 / 69s | Files written but commit failed |
|
|
91
|
+
| v1.5.0 W11-A1 | 14 / 80s | First commit landed, last two lost |
|
|
92
|
+
|
|
93
|
+
**8 of 13 dispatches = 62% truncation rate.** v1.5.0 S1 is the load-bearing fix for this class of failure.
|
|
94
|
+
|
|
95
|
+
## Worktree isolation drain protocol — v1.5.0-major S01
|
|
96
|
+
|
|
97
|
+
The v1.5.0 S1 checkpoint contract above assumes the subagent's `projectRoot` matches the orchestrator's `projectRoot`. That assumption **does not hold** for canonical dispatch in `isolation: 'worktree'` mode: the Claude Code harness spawns the subagent with `cwd` set to a disposable worktree (e.g. `.claude/worktrees/agent-<id>/`), so `process.cwd()` resolves to the worktree — not the parent project. Checkpoints written under the worktree's `.ijfw/` directory vanish the moment `git worktree remove` runs, taking truncation forensics with them.
|
|
98
|
+
|
|
99
|
+
**This was R3's #1 honest finding for v1.5.0:** without the fix below, S1 is shelfware in the canonical dispatch mode where it matters most.
|
|
100
|
+
|
|
101
|
+
### How the fix works
|
|
102
|
+
|
|
103
|
+
Two complementary mechanisms:
|
|
104
|
+
|
|
105
|
+
1. **Env-var passthrough (primary).** When the orchestrator/dispatcher spawns a worktree subagent, it MUST set:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
IJFW_PARENT_PROJECT_ROOT=<absolute path to the parent projectRoot>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`orchestrator/subagent-telemetry.js` (`recordCheckpoint`, `readLastCheckpoint`, `listOrphanedSubagents`) and `dispatch/checkpoint-cli.js` consult this env var first and fall back to the passed `projectRoot` when absent. Net effect: a worktree subagent's `ijfw checkpoint` write lands in the **parent** project's `.ijfw/wave-<id>/` directory automatically — visible to the orchestrator before and after worktree cleanup, no drain step required.
|
|
112
|
+
|
|
113
|
+
The `ijfw checkpoint` CLI also logs the resolved root to stderr (`ijfw checkpoint: writing to <root>/.ijfw/wave-<id>/`) so operators can see at a glance whether the env var was honored.
|
|
114
|
+
|
|
115
|
+
2. **Belt-and-suspenders drain (fallback).** When the dispatcher cannot set env vars (older Claude Code harness builds, manual `claude` invocation inside a worktree, third-party orchestrators), the subagent's checkpoint still lands in the worktree's `.ijfw/`. To prevent loss, the orchestrator MUST run the drain CLI **BEFORE** `git worktree remove`:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
ijfw worktree-drain <waveId> <worktreePath>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This copies every `<worktreePath>/.ijfw/wave-<waveId>/subagent-*.checkpoint.json` into the parent's `.ijfw/wave-<waveId>/`. Idempotent (safe to re-run). Returns `{ok:true, drained: N}` (or `{ok:true, drained: 0}` if the worktree has no checkpoints).
|
|
122
|
+
|
|
123
|
+
### Orchestrator scanning across active worktrees
|
|
124
|
+
|
|
125
|
+
`listOrphanedSubagents(waveId, projectRoot, additionalRoots)` accepts an optional `additionalRoots: string[]` argument — pass the list of active worktree paths (typically from `git worktree list --porcelain`) so the orchestrator sees in-flight checkpoints from worktree subagents that have NOT yet drained. Returned subId list is deduplicated across all scanned roots.
|
|
126
|
+
|
|
127
|
+
### Backward compatibility
|
|
128
|
+
|
|
129
|
+
- **Subagents without the env var set** continue to write to the passed `projectRoot` (which for legacy non-worktree dispatch is already correct).
|
|
130
|
+
- **Subagents in a worktree without the env var set** write to the worktree's own `.ijfw/` — the orchestrator's `ijfw worktree-drain` pre-cleanup step recovers them.
|
|
131
|
+
- **`listOrphanedSubagents` called without `additionalRoots`** preserves v1.5.0-S1 behavior (single-root scan).
|
|
132
|
+
|
|
133
|
+
### Why both mechanisms
|
|
134
|
+
|
|
135
|
+
Env passthrough is the right primary because it makes checkpoints visible **continuously**, not just at cleanup time (matters for live `wave-status` queries and mid-flight resume). The drain step exists because env passthrough requires harness cooperation that may not exist in every dispatch path; the drain ensures checkpoints survive even when the env var doesn't propagate.
|
|
136
|
+
|
|
137
|
+
### Wiring status
|
|
138
|
+
|
|
139
|
+
- `subagent-telemetry.js`, `checkpoint-cli.js`, `wave-cli.js` (`worktree-drain`) — wired in v1.5.0 W12-A0 S01.
|
|
140
|
+
- Dispatcher env passthrough — **TODO marker** in `dispatch/extension.js`; depends on Claude Code harness exposing an env-passthrough hook on `Agent({ isolation: 'worktree' })` spawn. Until that lands, orchestrators MUST rely on the drain step.
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debug-trident-trigger.js — v1.5.1: the LIVE production trigger for the
|
|
3
|
+
* Trident-powered debug loop (T29, debug-trident.js).
|
|
4
|
+
*
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
* WHY THIS MODULE EXISTS
|
|
7
|
+
* --------------------------------------------------------------------------
|
|
8
|
+
*
|
|
9
|
+
* debug-trident.js (`runDebugCampaign`) is the cross-AI debug pillar — when a
|
|
10
|
+
* single-lens hypothesis tree stalls, it dispatches codex + gemini lenses in
|
|
11
|
+
* parallel to generate competing hypotheses. v1.5.1 W2.C wired it into
|
|
12
|
+
* `runPostDone` (post-done-runner.js) — but `runPostDone` is itself NOT on the
|
|
13
|
+
* live subagent-completion path. The live path is the `subagent.post-done`
|
|
14
|
+
* verb in state-sdk.js, which calls `runSelfCheck`, never `runPostDone`. So
|
|
15
|
+
* T29 was "tested but never firing in production".
|
|
16
|
+
*
|
|
17
|
+
* This module is the genuine wiring. It exposes ONE entrypoint —
|
|
18
|
+
* `maybeFireDebugTrident` — designed to be called fire-and-forget from the
|
|
19
|
+
* live gate-failure branch of the `subagent.post-done` verb. It mirrors the
|
|
20
|
+
* A-Mem auto-linking precedent in memory/fts5.js exactly:
|
|
21
|
+
*
|
|
22
|
+
* - The caller does NOT await it. The verb's return value and timing are
|
|
23
|
+
* unchanged — STATE-SDK-CONTRACT §8 classes `subagent.post-done` as a
|
|
24
|
+
* fast read verb; an inline blocking multi-lens AI call would violate
|
|
25
|
+
* that contract. The campaign runs in the background.
|
|
26
|
+
* - Env-gated. `IJFW_DEBUG_TRIDENT=1` enables it; default is OFF. (A-Mem's
|
|
27
|
+
* auto-linker is default-ON with an `IJFW_AUTOLINK_OFF` kill switch;
|
|
28
|
+
* debug-trident spawns EXTERNAL codex/gemini processes, so the safe
|
|
29
|
+
* default for an unattended gate-failure path is opt-in — but the
|
|
30
|
+
* env-gate + silent-no-op shape is identical.)
|
|
31
|
+
* - Silent no-op on missing deps. No codex/gemini CLI reachable, no
|
|
32
|
+
* dispatcher, a thrown import — every failure mode resolves to a quiet
|
|
33
|
+
* skip. It NEVER throws into the caller.
|
|
34
|
+
* - Result persistence. The competing-hypotheses output is written to an
|
|
35
|
+
* append-only JSONL receipt under `.ijfw/receipts/debug-campaigns.jsonl`
|
|
36
|
+
* so the dashboard / next phase can read it. (mirrors receipts.js).
|
|
37
|
+
*
|
|
38
|
+
* --------------------------------------------------------------------------
|
|
39
|
+
* THE DISPATCHER
|
|
40
|
+
* --------------------------------------------------------------------------
|
|
41
|
+
*
|
|
42
|
+
* runDebugCampaign needs a `tridentDispatch({ lens, evidencePack,
|
|
43
|
+
* currentHypotheses }) => { lens, hypotheses }`. IJFW's production multi-lens
|
|
44
|
+
* dispatcher is `defaultConvergeDispatch` in cross-orchestrator.js (the same
|
|
45
|
+
* one runPhaseEConverge + ijfw_cross_audit_converge use to spawn real
|
|
46
|
+
* codex/gemini). We obtain it via a DYNAMIC import() — static import would
|
|
47
|
+
* create a require cycle (cross-orchestrator.js → state-sdk.js telemetry →
|
|
48
|
+
* back here), and truncation.js was wired the same way (state-sdk.js:1246,
|
|
49
|
+
* commit 75e5894). The adapter wraps `defaultConvergeDispatch` (an audit
|
|
50
|
+
* dispatcher returning `{ verdict, findings }`) into the hypothesis-gen shape
|
|
51
|
+
* runDebugCampaign expects: each audit finding becomes one competing
|
|
52
|
+
* hypothesis row.
|
|
53
|
+
*
|
|
54
|
+
* Zero new prod deps. ESM. Node ≥18. No emoji.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import fs from 'node:fs';
|
|
58
|
+
import path from 'node:path';
|
|
59
|
+
|
|
60
|
+
import { runDebugCampaign } from './debug-trident.js';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Env gate. `IJFW_DEBUG_TRIDENT=1` (or `true`/`on`) turns the live trigger
|
|
64
|
+
* on. Default OFF — the campaign spawns external codex/gemini processes, so
|
|
65
|
+
* an unattended gate-failure path stays opt-in. Read on every call so a test
|
|
66
|
+
* harness can flip it without re-importing.
|
|
67
|
+
*/
|
|
68
|
+
export function debugTridentEnabled() {
|
|
69
|
+
const v = String(process.env.IJFW_DEBUG_TRIDENT || '').trim().toLowerCase();
|
|
70
|
+
return v === '1' || v === 'true' || v === 'on' || v === 'yes';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Receipt path — append-only JSONL, sibling of the cross-run receipts file.
|
|
75
|
+
*/
|
|
76
|
+
export function debugCampaignReceiptPath(projectRoot) {
|
|
77
|
+
return path.join(projectRoot, '.ijfw', 'receipts', 'debug-campaigns.jsonl');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cap on receipt entries — same MAX_RECEIPTS posture as receipts.js so the
|
|
81
|
+
// file never grows unbounded under a flapping gate.
|
|
82
|
+
const MAX_DEBUG_RECEIPTS = 100;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append one debug-campaign record to the JSONL receipt. Best-effort: a
|
|
86
|
+
* write failure is swallowed (the campaign verdict is never altered by a
|
|
87
|
+
* diagnostic-write failure — mirrors fts5.js graph-errors.jsonl discipline).
|
|
88
|
+
*/
|
|
89
|
+
function writeDebugCampaignReceipt(projectRoot, record) {
|
|
90
|
+
try {
|
|
91
|
+
const dest = debugCampaignReceiptPath(projectRoot);
|
|
92
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
93
|
+
fs.appendFileSync(dest, JSON.stringify(record) + '\n');
|
|
94
|
+
// Prune to the last MAX_DEBUG_RECEIPTS lines.
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(dest, 'utf8');
|
|
97
|
+
const lines = raw.split('\n').filter((l) => l.trim());
|
|
98
|
+
if (lines.length > MAX_DEBUG_RECEIPTS) {
|
|
99
|
+
fs.writeFileSync(dest, lines.slice(-MAX_DEBUG_RECEIPTS).join('\n') + '\n');
|
|
100
|
+
}
|
|
101
|
+
} catch { /* prune is best-effort */ }
|
|
102
|
+
} catch { /* receipt write must never throw into the caller */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read all debug-campaign receipts for a project. Skips corrupt lines.
|
|
107
|
+
* Used by the integration test + (future) the dashboard.
|
|
108
|
+
*/
|
|
109
|
+
export function readDebugCampaignReceipts(projectRoot) {
|
|
110
|
+
const file = debugCampaignReceiptPath(projectRoot);
|
|
111
|
+
if (!fs.existsSync(file)) return [];
|
|
112
|
+
const out = [];
|
|
113
|
+
try {
|
|
114
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
115
|
+
for (const line of raw.split('\n')) {
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
try { out.push(JSON.parse(line)); } catch { /* skip malformed */ }
|
|
118
|
+
}
|
|
119
|
+
} catch { /* unreadable -> empty */ }
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the production `tridentDispatch` function runDebugCampaign needs.
|
|
125
|
+
*
|
|
126
|
+
* `defaultConvergeDispatch` (cross-orchestrator.js) is an AUDIT dispatcher:
|
|
127
|
+
* it spawns a lens CLI with an audit prompt and returns
|
|
128
|
+
* `{ lens, verdict, findings:[...] }`. runDebugCampaign instead wants a
|
|
129
|
+
* hypothesis generator returning `{ lens, hypotheses:[{hypothesis,rationale}] }`.
|
|
130
|
+
* The adapter bridges the two: the evidence pack is handed to the lens as the
|
|
131
|
+
* audit target, and each returned finding is mapped to one competing
|
|
132
|
+
* hypothesis (finding text → hypothesis, severity/category → rationale).
|
|
133
|
+
*
|
|
134
|
+
* A lens that is unreachable returns verdict UNREACHABLE / zero findings,
|
|
135
|
+
* which the adapter passes through as zero hypotheses — runDebugCampaign
|
|
136
|
+
* already treats that as a non-contributing lens (no crash).
|
|
137
|
+
*
|
|
138
|
+
* Returns `null` when the dispatcher cannot be loaded at all (missing
|
|
139
|
+
* module) — the caller treats that as a silent no-op.
|
|
140
|
+
*/
|
|
141
|
+
export async function buildTridentDispatch() {
|
|
142
|
+
let defaultConvergeDispatch;
|
|
143
|
+
try {
|
|
144
|
+
// DYNAMIC import — avoids a static require cycle through
|
|
145
|
+
// cross-orchestrator.js -> receipts/telemetry -> state-sdk.js.
|
|
146
|
+
({ defaultConvergeDispatch } = await import('../cross-orchestrator.js'));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (typeof defaultConvergeDispatch !== 'function') return null;
|
|
151
|
+
|
|
152
|
+
return async function tridentDispatch({ lens, evidencePack, currentHypotheses, signal } = {}) {
|
|
153
|
+
// Embed the already-tried hypotheses so the lens avoids re-proposing
|
|
154
|
+
// refuted theory (runDebugCampaign also dedups, this just saves tokens).
|
|
155
|
+
const triedBlock = Array.isArray(currentHypotheses) && currentHypotheses.length > 0
|
|
156
|
+
? '\n\n## Hypotheses already considered (propose DIFFERENT ones)\n'
|
|
157
|
+
+ currentHypotheses
|
|
158
|
+
.map((h) => `- ${h && h.hypothesis ? h.hypothesis : ''} `
|
|
159
|
+
+ `[${h && h.status ? h.status : 'open'}]`)
|
|
160
|
+
.join('\n')
|
|
161
|
+
: '';
|
|
162
|
+
const target = `## Stalled debug investigation — generate competing root-cause hypotheses\n\n`
|
|
163
|
+
+ `${typeof evidencePack === 'string' ? evidencePack : ''}${triedBlock}`;
|
|
164
|
+
let raw;
|
|
165
|
+
try {
|
|
166
|
+
raw = await defaultConvergeDispatch({
|
|
167
|
+
lens,
|
|
168
|
+
commitRange: target,
|
|
169
|
+
iteration: 1,
|
|
170
|
+
cycleSummary: null,
|
|
171
|
+
signal: signal || null,
|
|
172
|
+
});
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return { lens, hypotheses: [], ok: false, reason: err && err.message ? err.message : String(err) };
|
|
175
|
+
}
|
|
176
|
+
const findings = raw && Array.isArray(raw.findings) ? raw.findings : [];
|
|
177
|
+
const hypotheses = findings
|
|
178
|
+
.map((f) => {
|
|
179
|
+
if (!f || typeof f !== 'object') return null;
|
|
180
|
+
const text = typeof f.finding === 'string' && f.finding.trim()
|
|
181
|
+
? f.finding.trim()
|
|
182
|
+
: (typeof f.title === 'string' ? f.title.trim() : '');
|
|
183
|
+
if (!text) return null;
|
|
184
|
+
const rationaleParts = [];
|
|
185
|
+
if (f.severity) rationaleParts.push(`severity:${f.severity}`);
|
|
186
|
+
if (f.category) rationaleParts.push(`category:${f.category}`);
|
|
187
|
+
if (typeof f.rationale === 'string' && f.rationale.trim()) {
|
|
188
|
+
rationaleParts.push(f.rationale.trim());
|
|
189
|
+
}
|
|
190
|
+
return { hypothesis: text, rationale: rationaleParts.join(' ') };
|
|
191
|
+
})
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
return { lens, hypotheses };
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Fire-and-forget entrypoint — call this from the LIVE `subagent.post-done`
|
|
199
|
+
* gate-failure branch. It returns IMMEDIATELY; the actual campaign runs in a
|
|
200
|
+
* detached promise. The verb's return value and timing are unchanged.
|
|
201
|
+
*
|
|
202
|
+
* @param {object} opts
|
|
203
|
+
* @param {string} opts.projectRoot project root (where `.ijfw/` lives)
|
|
204
|
+
* @param {string} [opts.subagentId] the subagent whose gate failed
|
|
205
|
+
* @param {string} [opts.reason] the gate-failure reason string
|
|
206
|
+
* @param {string} [opts.reportText] the subagent's DONE report (evidence)
|
|
207
|
+
* @param {object} [opts.selfCheck] the failed self-check result (evidence)
|
|
208
|
+
* @returns {void} nothing — never throws
|
|
209
|
+
*
|
|
210
|
+
* Diagnostic hook: `maybeFireDebugTrident.__lastCampaignPromise` holds the
|
|
211
|
+
* most recent background promise so integration tests can `await` it before
|
|
212
|
+
* asserting on the receipt (mirrors `indexEntry.__lastAutoLinkPromise` in
|
|
213
|
+
* memory/fts5.js). Production callers do NOT read this.
|
|
214
|
+
*/
|
|
215
|
+
export function maybeFireDebugTrident(opts = {}) {
|
|
216
|
+
// Gate 1 — env opt-in. Disabled => true no-op. No promise, no receipt.
|
|
217
|
+
if (!debugTridentEnabled()) {
|
|
218
|
+
maybeFireDebugTrident.__lastCampaignPromise = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const projectRoot = typeof opts.projectRoot === 'string' && opts.projectRoot
|
|
222
|
+
? opts.projectRoot : null;
|
|
223
|
+
if (!projectRoot) {
|
|
224
|
+
maybeFireDebugTrident.__lastCampaignPromise = null;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const subagentId = typeof opts.subagentId === 'string' && opts.subagentId
|
|
229
|
+
? opts.subagentId : 'unknown';
|
|
230
|
+
const reason = typeof opts.reason === 'string' ? opts.reason : 'gate-failure';
|
|
231
|
+
const reportText = typeof opts.reportText === 'string' ? opts.reportText : '';
|
|
232
|
+
const selfCheck = opts.selfCheck && typeof opts.selfCheck === 'object'
|
|
233
|
+
? opts.selfCheck : null;
|
|
234
|
+
// A test-supplied dispatcher (stub) short-circuits the dynamic import —
|
|
235
|
+
// lets the integration test prove the wiring fires WITHOUT spawning real
|
|
236
|
+
// codex/gemini. Production callers never pass this.
|
|
237
|
+
const injectedDispatch = typeof opts.tridentDispatch === 'function'
|
|
238
|
+
? opts.tridentDispatch : null;
|
|
239
|
+
|
|
240
|
+
// Compose the evidence pack from the gate-failure context. This is what
|
|
241
|
+
// the codex/gemini lenses reason over.
|
|
242
|
+
const evidenceLines = [
|
|
243
|
+
`Subagent ${subagentId} failed its post-done self-check gate.`,
|
|
244
|
+
`Gate-failure reason: ${reason}`,
|
|
245
|
+
];
|
|
246
|
+
if (selfCheck) {
|
|
247
|
+
if (Array.isArray(selfCheck.files_missing) && selfCheck.files_missing.length) {
|
|
248
|
+
evidenceLines.push(`Missing claimed files: ${selfCheck.files_missing.join(', ')}`);
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(selfCheck.commits_missing) && selfCheck.commits_missing.length) {
|
|
251
|
+
evidenceLines.push(`Missing claimed commits: ${selfCheck.commits_missing.join(', ')}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (reportText) {
|
|
255
|
+
evidenceLines.push('', '--- Subagent DONE report ---', reportText.slice(0, 4000));
|
|
256
|
+
}
|
|
257
|
+
const evidencePack = evidenceLines.join('\n');
|
|
258
|
+
|
|
259
|
+
// The seed hypothesis — the single-lens reading that "stalled" (the gate
|
|
260
|
+
// failed). Trident dispatches codex+gemini for competing alternatives.
|
|
261
|
+
const seedHypotheses = [
|
|
262
|
+
{
|
|
263
|
+
id: 'H1',
|
|
264
|
+
hypothesis: `Subagent ${subagentId} reported DONE but did not produce the claimed artifacts (${reason}).`,
|
|
265
|
+
status: 'open',
|
|
266
|
+
evidence: reason,
|
|
267
|
+
refuted_by: '',
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
// Fire-and-forget — NOT awaited. Any failure resolves to a quiet skip.
|
|
272
|
+
const campaignPromise = (async () => {
|
|
273
|
+
let tridentDispatch = injectedDispatch;
|
|
274
|
+
if (!tridentDispatch) {
|
|
275
|
+
tridentDispatch = await buildTridentDispatch();
|
|
276
|
+
}
|
|
277
|
+
if (typeof tridentDispatch !== 'function') {
|
|
278
|
+
// No dispatcher (missing module / missing CLIs) — silent no-op.
|
|
279
|
+
writeDebugCampaignReceipt(projectRoot, {
|
|
280
|
+
ts: new Date().toISOString(),
|
|
281
|
+
subagentId,
|
|
282
|
+
reason,
|
|
283
|
+
outcome: 'skipped',
|
|
284
|
+
skipReason: 'no-trident-dispatcher',
|
|
285
|
+
});
|
|
286
|
+
return { skipped: true, skipReason: 'no-trident-dispatcher' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// The per-cycle `dispatch` for the live trigger: cycle 1 reports the
|
|
290
|
+
// single-lens stall (INVESTIGATION_INCONCLUSIVE) so the campaign
|
|
291
|
+
// immediately escalates to Trident; cycle 2 reports inconclusive again
|
|
292
|
+
// so the campaign terminates cleanly once competing hypotheses exist.
|
|
293
|
+
// The point of the LIVE trigger is to GENERATE the competing-hypotheses
|
|
294
|
+
// set off a real gate failure, not to auto-resolve the bug — resolution
|
|
295
|
+
// is the ijfw-debugger agent's job, this just seeds it.
|
|
296
|
+
let stallCycles = 0;
|
|
297
|
+
const dispatch = async () => {
|
|
298
|
+
stallCycles += 1;
|
|
299
|
+
return { terminator: 'INVESTIGATION_INCONCLUSIVE' };
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
let campaign;
|
|
303
|
+
try {
|
|
304
|
+
campaign = await runDebugCampaign({
|
|
305
|
+
sessionId: `gate-failure-${subagentId}`,
|
|
306
|
+
symptoms: `post-done self-check FAILED for ${subagentId} — ${reason}`,
|
|
307
|
+
hypotheses: seedHypotheses,
|
|
308
|
+
dispatch,
|
|
309
|
+
tridentDispatch,
|
|
310
|
+
tridentLenses: ['codex', 'gemini'],
|
|
311
|
+
maxCycles: 2,
|
|
312
|
+
evidencePack,
|
|
313
|
+
projectRoot,
|
|
314
|
+
// recordTelemetry stays on (default) — the campaign also writes a
|
|
315
|
+
// telemetry.record via the state-SDK, same as the unit-tested path.
|
|
316
|
+
});
|
|
317
|
+
} catch (err) {
|
|
318
|
+
writeDebugCampaignReceipt(projectRoot, {
|
|
319
|
+
ts: new Date().toISOString(),
|
|
320
|
+
subagentId,
|
|
321
|
+
reason,
|
|
322
|
+
outcome: 'failed',
|
|
323
|
+
error: err && err.message ? err.message : String(err),
|
|
324
|
+
});
|
|
325
|
+
return { skipped: false, error: err && err.message ? err.message : String(err) };
|
|
326
|
+
}
|
|
327
|
+
void stallCycles;
|
|
328
|
+
|
|
329
|
+
// Persist the competing-hypotheses output. This is the receipt the
|
|
330
|
+
// dashboard / next phase reads — the campaign output is NOT lost.
|
|
331
|
+
const competing = Array.isArray(campaign.hypothesesFinal)
|
|
332
|
+
? campaign.hypothesesFinal.filter(
|
|
333
|
+
(h) => h && typeof h.from === 'string' && h.from.startsWith('trident:'),
|
|
334
|
+
)
|
|
335
|
+
: [];
|
|
336
|
+
writeDebugCampaignReceipt(projectRoot, {
|
|
337
|
+
ts: new Date().toISOString(),
|
|
338
|
+
subagentId,
|
|
339
|
+
reason,
|
|
340
|
+
outcome: campaign.outcome,
|
|
341
|
+
sessionId: campaign.sessionId,
|
|
342
|
+
cycles: campaign.cycles,
|
|
343
|
+
stalls: campaign.stalls,
|
|
344
|
+
tridentInvocations: campaign.tridentInvocations,
|
|
345
|
+
hypothesesAdded: campaign.hypothesesAdded,
|
|
346
|
+
competingHypotheses: competing.map((h) => ({
|
|
347
|
+
id: h.id,
|
|
348
|
+
from: h.from,
|
|
349
|
+
hypothesis: h.hypothesis,
|
|
350
|
+
rationale: h.rationale || '',
|
|
351
|
+
})),
|
|
352
|
+
durationMs: campaign.duration_ms,
|
|
353
|
+
});
|
|
354
|
+
return { skipped: false, campaign };
|
|
355
|
+
})().catch((err) => {
|
|
356
|
+
// Last-resort guard — the background promise must NEVER surface an
|
|
357
|
+
// unhandled rejection. Best-effort receipt, then swallow.
|
|
358
|
+
try {
|
|
359
|
+
writeDebugCampaignReceipt(projectRoot, {
|
|
360
|
+
ts: new Date().toISOString(),
|
|
361
|
+
subagentId,
|
|
362
|
+
reason,
|
|
363
|
+
outcome: 'failed',
|
|
364
|
+
error: err && err.message ? err.message : String(err),
|
|
365
|
+
});
|
|
366
|
+
} catch { /* nothing more we can do */ }
|
|
367
|
+
return { skipped: false, error: err && err.message ? err.message : String(err) };
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Expose for deterministic test awaiting only.
|
|
371
|
+
maybeFireDebugTrident.__lastCampaignPromise = campaignPromise;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
maybeFireDebugTrident.__lastCampaignPromise = null;
|