@ijfw/memory-server 1.4.4 → 1.5.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.
- 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 +1 -1
- 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 +550 -14
- package/src/cross-orchestrator.js +1016 -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/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 +554 -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 +152 -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/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/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- 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.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 +249 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/runtime-loop.js +430 -0
- 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 +1764 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +452 -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-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +94 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -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 +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "fx-05-error-terminated-03",
|
|
3
|
+
"category": "error-terminated",
|
|
4
|
+
"waveId": "W20-errT-3",
|
|
5
|
+
"subId": "errT3",
|
|
6
|
+
"notes": "error-terminated with overwrite partial — rollback restores baseline",
|
|
7
|
+
"expectedDetection": {
|
|
8
|
+
"truncated": "error-terminated",
|
|
9
|
+
"reasonContains": "outcome='error'"
|
|
10
|
+
},
|
|
11
|
+
"expectedRecovery": {
|
|
12
|
+
"recovered": true
|
|
13
|
+
},
|
|
14
|
+
"expectedFinalState": {
|
|
15
|
+
".ijfw/state/workflow.json": "{\"phase\":\"errT-pre-3\"}"
|
|
16
|
+
},
|
|
17
|
+
"expectedTerminalVerb": "subagent.post-done"
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"phase":"errT-PARTIAL-3"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"seq":1,"verb":"subagent.checkpoint","subagentId":"errT4","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-4-ckpt","outcome":"ok","payloadDigest":"sha256-fixture-v-errT-4-ckpt"}
|
|
2
|
+
{"seq":2,"verb":"decision.add","subagentId":"errT4","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-4-partial","outcome":"error","payloadDigest":"sha256-fixture-v-errT-4-partial"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"verb":"subagent.checkpoint","verbId":"v-errT-4-ckpt","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/wave-W20-errT-4/subagent-errT4.checkpoint.json"],"payloadDigest":"sha256-fixture-v-errT-4-ckpt","kind":"append","dedupKey":"dk-ckpt-errT-4"}
|
|
2
|
+
{"verb":"subagent.checkpoint","verbId":"v-errT-4-ckpt","phase":"commit","ts":"2026-05-20T12:00:00.000Z","payloadDigest":"sha256-fixture-v-errT-4-ckpt","kind":"append","dedupKey":"dk-ckpt-errT-4"}
|
|
3
|
+
{"verb":"decision.add","verbId":"v-errT-4-partial","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/blackboard/decisions.jsonl"],"payloadDigest":"sha256-fixture-v-errT-4-partial","kind":"append","dedupKey":"dk-errT-4"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "fx-05-error-terminated-04",
|
|
3
|
+
"category": "error-terminated",
|
|
4
|
+
"waveId": "W20-errT-4",
|
|
5
|
+
"subId": "errT4",
|
|
6
|
+
"notes": "error-terminated with append partial — seal-in-place preserves record",
|
|
7
|
+
"expectedDetection": {
|
|
8
|
+
"truncated": "error-terminated",
|
|
9
|
+
"reasonContains": "outcome='error'"
|
|
10
|
+
},
|
|
11
|
+
"expectedRecovery": {
|
|
12
|
+
"recovered": true
|
|
13
|
+
},
|
|
14
|
+
"expectedFinalState": {
|
|
15
|
+
".ijfw/blackboard/decisions.jsonl": "{\"id\":\"d-errT-4\",\"text\":\"errT 4\",\"dedupKey\":\"dk-errT-4\"}\n"
|
|
16
|
+
},
|
|
17
|
+
"expectedTerminalVerb": "subagent.post-done"
|
|
18
|
+
}
|
package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"d-errT-4","text":"errT 4","dedupKey":"dk-errT-4"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"seq":1,"verb":"workflow.set-phase","subagentId":"errT5","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-5-ok","outcome":"ok","payloadDigest":"sha256-fixture-v-errT-5-ok"}
|
|
2
|
+
{"seq":2,"verb":"wave.advance","subagentId":"errT5","ts":"2026-05-20T12:00:00.000Z","verbId":"v-errT-5-partial","outcome":"error","payloadDigest":"sha256-fixture-v-errT-5-partial"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"verb":"workflow.set-phase","verbId":"v-errT-5-ok","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/state/workflow.json"],"payloadDigest":"sha256-fixture-v-errT-5-ok","kind":"overwrite"}
|
|
2
|
+
{"verb":"workflow.set-phase","verbId":"v-errT-5-ok","phase":"commit","ts":"2026-05-20T12:00:00.000Z","payloadDigest":"sha256-fixture-v-errT-5-ok","kind":"overwrite"}
|
|
3
|
+
{"verb":"wave.advance","verbId":"v-errT-5-partial","phase":"begin","ts":"2026-05-20T12:00:00.000Z","targets":[".ijfw/state/workflow.json"],"payloadDigest":"sha256-fixture-v-errT-5-partial","kind":"overwrite"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "fx-05-error-terminated-05",
|
|
3
|
+
"category": "error-terminated",
|
|
4
|
+
"waveId": "W20-errT-5",
|
|
5
|
+
"subId": "errT5",
|
|
6
|
+
"notes": "error-terminated with overwrite partial — rollback restores baseline",
|
|
7
|
+
"expectedDetection": {
|
|
8
|
+
"truncated": "error-terminated",
|
|
9
|
+
"reasonContains": "outcome='error'"
|
|
10
|
+
},
|
|
11
|
+
"expectedRecovery": {
|
|
12
|
+
"recovered": true
|
|
13
|
+
},
|
|
14
|
+
"expectedFinalState": {
|
|
15
|
+
".ijfw/state/workflow.json": "{\"phase\":\"errT-pre-5\"}"
|
|
16
|
+
},
|
|
17
|
+
"expectedTerminalVerb": "subagent.post-done"
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"phase":"errT-PARTIAL-5"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ijfw/memory-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Cross-platform persistent memory server for IJFW. 10 MCP tools (memory + admin/update). Works with 13 MCP-using platforms (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Copilot, Hermes, Wayland, OpenCode, QwenCode, Cline, KimiCode, OpenClaw) plus Aider via rules-only tier.",
|
|
5
5
|
"author": "Sean Donahoe",
|
|
6
6
|
"license": "MIT",
|
|
@@ -5,14 +5,38 @@
|
|
|
5
5
|
* clears it on deactivate. Used by:
|
|
6
6
|
* - `ijfw_run extension:activate <name>` CLI command
|
|
7
7
|
* - installExtension when opts.activate is set
|
|
8
|
+
*
|
|
9
|
+
* v1.5.0 T10 — Migrated to the state-SDK. The legacy
|
|
10
|
+
* `writeFile`/`rename`/`unlink` direct-write path is replaced by a call to
|
|
11
|
+
* `query('extension.set-active', ...)`. The state-SDK is the single mutation
|
|
12
|
+
* surface for `~/.ijfw/state/active-extension.json`; this writer now adapts
|
|
13
|
+
* the SDK's verb-returns to the writer's `{ok, path, error}` /
|
|
14
|
+
* `{ok, removed}` external API so every existing caller keeps working without
|
|
15
|
+
* change (`extension-installer.js`, `dispatch/active-cli.js`,
|
|
16
|
+
* `dispatch/extension.js`, the test suite).
|
|
17
|
+
*
|
|
18
|
+
* Side-effects retained verbatim from the v1.4.0/W7.1 contract:
|
|
19
|
+
* - quota counters are reset on activate (B16/SEC-M-02) and clear (B16) via
|
|
20
|
+
* `resetExtensionQuotas` — best-effort, never blocks the verb result.
|
|
21
|
+
* - B18 cross-IDE last-seen / divergence detection (`detectCrossIdeDivergence`)
|
|
22
|
+
* stays as-is — it is a read-only consumer of the homedir state file and
|
|
23
|
+
* does not need the SDK.
|
|
24
|
+
* - `findInstalledManifest` stays as-is — it reads project/org/user-scope
|
|
25
|
+
* manifest.json files; it is a read of installed extensions, not a write
|
|
26
|
+
* to the homedir state file, so it is outside the state-SDK surface.
|
|
8
27
|
*/
|
|
9
28
|
|
|
10
29
|
import { readFile, writeFile, unlink, mkdir, readdir, stat } from 'node:fs/promises';
|
|
30
|
+
import { existsSync } from 'node:fs';
|
|
11
31
|
import { join, dirname } from 'node:path';
|
|
12
32
|
import { homedir } from 'node:os';
|
|
13
|
-
import { randomBytes } from 'node:crypto';
|
|
14
33
|
|
|
15
34
|
import { resetExtensionQuotas } from './extension-quota-tracker.js';
|
|
35
|
+
// v1.5.0 audit-LOW-update-#13: shared tmp-suffix helper.
|
|
36
|
+
import { tmpSuffix } from './lib/tmp-suffix.js';
|
|
37
|
+
// v1.5.0 T10: route the canonical write/clear of ~/.ijfw/state/active-extension.json
|
|
38
|
+
// through the state-SDK verb.
|
|
39
|
+
import { query } from './orchestrator/state-sdk.js';
|
|
16
40
|
|
|
17
41
|
const STATE_PATH_REL = ['.ijfw', 'state', 'active-extension.json'];
|
|
18
42
|
|
|
@@ -34,16 +58,37 @@ function stateDir(home) {
|
|
|
34
58
|
return join(home || homedir(), '.ijfw', 'state');
|
|
35
59
|
}
|
|
36
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Pick a state-SDK projectRoot to satisfy the verb's `ctx.projectRoot`
|
|
63
|
+
* requirement. The writer's external callers don't carry a project root
|
|
64
|
+
* (extension activation is a homedir-scoped operation), so we fall back to
|
|
65
|
+
* `process.cwd()`. The SDK uses this only for the intent-journal lock path
|
|
66
|
+
* (`<projectRoot>/.ijfw/state/intent-journal.jsonl`) — it never mutates any
|
|
67
|
+
* project file under it. opts.projectRoot, when supplied, takes precedence.
|
|
68
|
+
*/
|
|
69
|
+
function pickProjectRoot(opts) {
|
|
70
|
+
if (opts && typeof opts.projectRoot === 'string' && opts.projectRoot.length > 0) {
|
|
71
|
+
return opts.projectRoot;
|
|
72
|
+
}
|
|
73
|
+
return process.cwd();
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
/**
|
|
38
77
|
* Write the active-extension state file from a manifest + scope.
|
|
39
|
-
* Validates required fields before write. Atomic write via
|
|
78
|
+
* Validates required fields before write. Atomic write via the state-SDK
|
|
79
|
+
* `extension.set-active` verb (tmp+rename via atomic-io).
|
|
40
80
|
*
|
|
41
|
-
* @param {{ name: string, permissions: { reads: string[], writes: string[] } }} manifest
|
|
81
|
+
* @param {{ name: string, permissions: { reads: string[], writes: string[] }, quotas?: object }} manifest
|
|
42
82
|
* @param {'project'|'org'|'user'} scope
|
|
43
|
-
* @param {{ homeDir?: string, ideId?: string|null }} [opts]
|
|
83
|
+
* @param {{ homeDir?: string, ideId?: string|null, projectRoot?: string }} [opts]
|
|
44
84
|
* @returns {Promise<{ ok: boolean, path?: string, error?: string }>}
|
|
45
85
|
*/
|
|
46
86
|
export async function writeActiveExtension(manifest, scope, opts = {}) {
|
|
87
|
+
// Validation — external API contract (returns ok:false, never throws).
|
|
88
|
+
// Mirrors the pre-v1.5.0 behavior so every existing caller keeps the same
|
|
89
|
+
// error-handling shape. The SDK verb would throw on the same inputs; the
|
|
90
|
+
// adapter catches that downstream too, but explicit pre-checks give the
|
|
91
|
+
// historical error messages.
|
|
47
92
|
if (!manifest || typeof manifest !== 'object') {
|
|
48
93
|
return { ok: false, error: 'manifest must be an object' };
|
|
49
94
|
}
|
|
@@ -56,57 +101,73 @@ export async function writeActiveExtension(manifest, scope, opts = {}) {
|
|
|
56
101
|
if (!manifest.permissions || typeof manifest.permissions !== 'object') {
|
|
57
102
|
return { ok: false, error: 'manifest.permissions required' };
|
|
58
103
|
}
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
104
|
+
|
|
105
|
+
const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
|
|
106
|
+
|
|
62
107
|
// B18: stamp activated_by_ide + activated_by_pid when ideId is provided.
|
|
63
108
|
// Caller (CLI) is responsible for calling detectIde() and threading the
|
|
64
|
-
// value in. When opts.ideId is null/undefined, fields are
|
|
65
|
-
// file stays back-compatible with v1.4.1 readers).
|
|
109
|
+
// value in. When opts.ideId is null/undefined or invalid, fields are
|
|
110
|
+
// omitted (so the file stays back-compatible with v1.4.1 readers).
|
|
66
111
|
const ideId = (typeof opts.ideId === 'string' && IDE_ID_PATTERN.test(opts.ideId))
|
|
67
112
|
? opts.ideId
|
|
68
113
|
: null;
|
|
69
|
-
|
|
114
|
+
|
|
115
|
+
// Normalize permissions + quotas in the manifest payload so the SDK verb
|
|
116
|
+
// sees the exact shape it expects. The verb itself filters quotas to
|
|
117
|
+
// positive integers — we leave that filtering to it (single writer rule).
|
|
118
|
+
const reads = Array.isArray(manifest.permissions.reads) ? manifest.permissions.reads : [];
|
|
119
|
+
const writes = Array.isArray(manifest.permissions.writes) ? manifest.permissions.writes : [];
|
|
120
|
+
const verbManifest = {
|
|
70
121
|
name: manifest.name,
|
|
71
|
-
scope,
|
|
72
122
|
permissions: { reads, writes },
|
|
73
|
-
activated_at: activatedAt,
|
|
74
123
|
};
|
|
75
|
-
if (ideId) {
|
|
76
|
-
out.activated_by_ide = ideId;
|
|
77
|
-
out.activated_by_pid = process.pid;
|
|
78
|
-
}
|
|
79
|
-
// R12-H-01: persist manifest.quotas so the tier-2 hook
|
|
80
|
-
// (extension-permission-check.mjs) can enforce quotas on Edit/Write/Bash
|
|
81
|
-
// dispatch. Without this the tier-2 hook reads `active.quotas` as undefined
|
|
82
|
-
// and silently bypasses the v1.4.3 quota gate that the server-side
|
|
83
|
-
// gatePermissionAndQuota path enforces. Schema (extension-manifest-schema.js):
|
|
84
|
-
// optional object whose values are positive integers — currently
|
|
85
|
-
// max_files_written / max_bytes_written / max_wall_clock_ms (forward-compat:
|
|
86
|
-
// unknown dimensions are kept as-is — schema rejects unknowns at install).
|
|
87
124
|
if (
|
|
88
125
|
manifest.quotas !== undefined &&
|
|
89
126
|
manifest.quotas !== null &&
|
|
90
127
|
typeof manifest.quotas === 'object' &&
|
|
91
128
|
!Array.isArray(manifest.quotas)
|
|
92
129
|
) {
|
|
93
|
-
|
|
94
|
-
let copied = 0;
|
|
95
|
-
for (const [k, v] of Object.entries(manifest.quotas)) {
|
|
96
|
-
if (typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v) && v > 0) {
|
|
97
|
-
cleanQuotas[k] = v;
|
|
98
|
-
copied++;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (copied > 0) out.quotas = cleanQuotas;
|
|
130
|
+
verbManifest.quotas = manifest.quotas;
|
|
102
131
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
|
|
133
|
+
const payload = {
|
|
134
|
+
manifest: verbManifest,
|
|
135
|
+
scope,
|
|
136
|
+
homeDir: home,
|
|
137
|
+
};
|
|
138
|
+
if (ideId) {
|
|
139
|
+
payload.activated_by_ide = ideId;
|
|
140
|
+
payload.activated_by_pid = process.pid;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let result;
|
|
144
|
+
try {
|
|
145
|
+
result = await query('extension.set-active', payload, {
|
|
146
|
+
projectRoot: pickProjectRoot(opts),
|
|
147
|
+
homeDir: home,
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// The SDK verb throws on protocol violations. Surface as the writer's
|
|
151
|
+
// {ok:false, error} contract so callers don't have to learn a new shape.
|
|
152
|
+
return { ok: false, error: err && err.message ? err.message : String(err) };
|
|
153
|
+
}
|
|
154
|
+
if (!result || result.ok !== true) {
|
|
155
|
+
return { ok: false, error: 'state-sdk extension.set-active returned non-ok' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Recover the activated_at timestamp the SDK stamped so we can pass it to
|
|
159
|
+
// resetExtensionQuotas (parity with the legacy writer's wall_clock_ms window).
|
|
160
|
+
// Best-effort: if the read fails, omit activated_at and the tracker resets
|
|
161
|
+
// counters without the window stamp.
|
|
162
|
+
let activatedAt;
|
|
163
|
+
try {
|
|
164
|
+
const raw = await readFile(result.path, 'utf8');
|
|
165
|
+
const parsed = JSON.parse(raw);
|
|
166
|
+
if (parsed && typeof parsed.activated_at === 'string') {
|
|
167
|
+
activatedAt = parsed.activated_at;
|
|
168
|
+
}
|
|
169
|
+
} catch { /* best-effort */ }
|
|
170
|
+
|
|
110
171
|
// B16/SEC-M-02: reset quota counters on activate; stamp activated_at so
|
|
111
172
|
// wall_clock_ms can be computed against this activation window.
|
|
112
173
|
try {
|
|
@@ -115,21 +176,24 @@ export async function writeActiveExtension(manifest, scope, opts = {}) {
|
|
|
115
176
|
// Quota reset failure must not block activation. Counters will self-heal
|
|
116
177
|
// on next deactivate or the next activate of the same name.
|
|
117
178
|
}
|
|
118
|
-
|
|
179
|
+
|
|
180
|
+
return { ok: true, path: result.path };
|
|
119
181
|
}
|
|
120
182
|
|
|
121
183
|
/**
|
|
122
184
|
* Clear the active-extension state file. Idempotent -- succeeds if file is absent.
|
|
123
185
|
*
|
|
124
|
-
* @param {{ homeDir?: string }} [opts]
|
|
186
|
+
* @param {{ homeDir?: string, projectRoot?: string }} [opts]
|
|
125
187
|
* @returns {Promise<{ ok: boolean, removed: boolean }>}
|
|
126
188
|
*/
|
|
127
189
|
export async function clearActiveExtension(opts = {}) {
|
|
128
190
|
const home = opts && opts.homeDir ? opts.homeDir : (process.env.HOME || homedir());
|
|
129
|
-
|
|
130
|
-
//
|
|
191
|
+
|
|
192
|
+
// B16/SEC-M-02: read the active extension name BEFORE clearing so we can
|
|
193
|
+
// reset its quota counters. Best-effort: if the file is missing or
|
|
131
194
|
// malformed, deactivate still succeeds.
|
|
132
195
|
let extName = null;
|
|
196
|
+
const wasPresent = existsSync(statePath(home));
|
|
133
197
|
try {
|
|
134
198
|
const raw = await readFile(statePath(home), 'utf8');
|
|
135
199
|
const parsed = JSON.parse(raw);
|
|
@@ -139,27 +203,42 @@ export async function clearActiveExtension(opts = {}) {
|
|
|
139
203
|
} catch {
|
|
140
204
|
// ignore — extName stays null
|
|
141
205
|
}
|
|
206
|
+
|
|
207
|
+
// Drive the canonical clear through the SDK verb (manifest:null is the
|
|
208
|
+
// documented clear semantics — see STATE-SDK-CONTRACT.md §7).
|
|
209
|
+
// The verb is idempotent on an absent file (best-effort unlink).
|
|
210
|
+
let verbOk = false;
|
|
142
211
|
try {
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
212
|
+
const r = await query('extension.set-active', {
|
|
213
|
+
manifest: null,
|
|
214
|
+
// The verb requires a valid scope. 'project' is a safe sentinel — the
|
|
215
|
+
// clear branch does not consult `scope` for any state-file content
|
|
216
|
+
// (clear unlinks the file unconditionally). Any of the three valid
|
|
217
|
+
// values would work.
|
|
218
|
+
scope: 'project',
|
|
219
|
+
homeDir: home,
|
|
220
|
+
}, {
|
|
221
|
+
projectRoot: pickProjectRoot(opts),
|
|
222
|
+
homeDir: home,
|
|
223
|
+
});
|
|
224
|
+
verbOk = !!(r && r.ok);
|
|
225
|
+
} catch {
|
|
226
|
+
verbOk = false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Always reset quota counters if we know the ext name, regardless of
|
|
230
|
+
// whether the verb succeeded or the file was present — mirrors the
|
|
231
|
+
// legacy writer's ENOENT-tolerant best-effort reset.
|
|
232
|
+
if (extName) {
|
|
233
|
+
try {
|
|
234
|
+
await resetExtensionQuotas(extName, { homeDir: home });
|
|
235
|
+
} catch { /* best-effort */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (verbOk) {
|
|
239
|
+
return { ok: true, removed: wasPresent };
|
|
162
240
|
}
|
|
241
|
+
return { ok: false, removed: false };
|
|
163
242
|
}
|
|
164
243
|
|
|
165
244
|
/**
|
|
@@ -247,7 +326,8 @@ async function writeLastSeen(ideId, opts = {}) {
|
|
|
247
326
|
try {
|
|
248
327
|
await mkdir(dirname(path), { recursive: true });
|
|
249
328
|
const body = JSON.stringify({ ide: ideId, last_seen_at: new Date().toISOString() }, null, 2) + '\n';
|
|
250
|
-
|
|
329
|
+
// v1.5.0 audit-LOW-update-#13: tmpSuffix() replaces inline randomBytes call.
|
|
330
|
+
const tmp = `${path}.tmp.${tmpSuffix({ bytes: 4, includePid: false })}`;
|
|
251
331
|
await writeFile(tmp, body, 'utf8');
|
|
252
332
|
const { rename } = await import('node:fs/promises');
|
|
253
333
|
await rename(tmp, path);
|
package/src/api-client.js
CHANGED
|
@@ -8,6 +8,19 @@ import { getTemplate } from './cross-dispatcher.js';
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
10
10
|
|
|
11
|
+
// v1.5.0 audit-DISPUTED-1 — user-message cache_control threshold.
|
|
12
|
+
//
|
|
13
|
+
// Anthropic prompt-caching has a server-side minimum (~1024 tokens). We
|
|
14
|
+
// gate the user-content-block split on a conservative 2KB byte cutoff so
|
|
15
|
+
// short audits (which would never recover the cache-write overhead)
|
|
16
|
+
// remain plain strings, while large diff/transcript targets (which DO
|
|
17
|
+
// amortize the cache) get split into a cacheable prefix + ephemeral tail.
|
|
18
|
+
//
|
|
19
|
+
// Pairs with the H4.2 ordering invariant: cycleSummary (or any per-turn
|
|
20
|
+
// content) MUST land AFTER the cacheable block so it never busts the
|
|
21
|
+
// cache prefix. See ADJUDICATIONS.md DISPUTED-1.
|
|
22
|
+
const CACHE_USER_THRESHOLD_BYTES = 2048;
|
|
23
|
+
|
|
11
24
|
// ---------------------------------------------------------------------------
|
|
12
25
|
// Provider request builders
|
|
13
26
|
// ---------------------------------------------------------------------------
|
|
@@ -68,7 +81,7 @@ function buildGemini(system, user, model, key, timeoutMs, endpoint) {
|
|
|
68
81
|
// Sonnet 4.5 prompt-caching threshold: 1024 tokens (rough: chars / 4).
|
|
69
82
|
const CACHE_TOKEN_THRESHOLD = 1024;
|
|
70
83
|
|
|
71
|
-
function buildAnthropic(system, user, model, key, timeoutMs) {
|
|
84
|
+
function buildAnthropic(system, user, model, key, timeoutMs, cycleSummary = null) {
|
|
72
85
|
const promptChars = system.length + user.length;
|
|
73
86
|
const estimatedTokens = Math.floor(promptChars / 4);
|
|
74
87
|
const cacheEligible = estimatedTokens >= CACHE_TOKEN_THRESHOLD;
|
|
@@ -77,6 +90,26 @@ function buildAnthropic(system, user, model, key, timeoutMs) {
|
|
|
77
90
|
? [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }]
|
|
78
91
|
: system;
|
|
79
92
|
|
|
93
|
+
// v1.5.0 audit-DISPUTED-1 — user-message cache_control.
|
|
94
|
+
//
|
|
95
|
+
// When the user content is large enough to amortize the cache-write
|
|
96
|
+
// overhead, split into a content-blocks array: the stable target gets
|
|
97
|
+
// cache_control:{type:'ephemeral'}, and any per-turn tail (cycleSummary)
|
|
98
|
+
// follows as a plain text block so it never busts the cache prefix.
|
|
99
|
+
//
|
|
100
|
+
// Below the threshold we keep the legacy plain-string form -- no need
|
|
101
|
+
// to spend cache-write tokens we can't recover.
|
|
102
|
+
const userBytes = Buffer.byteLength(user, 'utf8');
|
|
103
|
+
let userContent;
|
|
104
|
+
if (userBytes >= CACHE_USER_THRESHOLD_BYTES) {
|
|
105
|
+
const cacheableBlock = { type: 'text', text: user, cache_control: { type: 'ephemeral' } };
|
|
106
|
+
userContent = cycleSummary
|
|
107
|
+
? [cacheableBlock, { type: 'text', text: cycleSummary }]
|
|
108
|
+
: [cacheableBlock];
|
|
109
|
+
} else {
|
|
110
|
+
userContent = user;
|
|
111
|
+
}
|
|
112
|
+
|
|
80
113
|
return {
|
|
81
114
|
url: 'https://api.anthropic.com/v1/messages',
|
|
82
115
|
options: {
|
|
@@ -91,7 +124,7 @@ function buildAnthropic(system, user, model, key, timeoutMs) {
|
|
|
91
124
|
model,
|
|
92
125
|
max_tokens: 4096,
|
|
93
126
|
system: systemBlock,
|
|
94
|
-
messages: [{ role: 'user', content:
|
|
127
|
+
messages: [{ role: 'user', content: userContent }],
|
|
95
128
|
}),
|
|
96
129
|
signal: AbortSignal.timeout(timeoutMs),
|
|
97
130
|
},
|
|
@@ -138,9 +171,13 @@ function extractCacheStats(json, cacheEligible) {
|
|
|
138
171
|
// Main export
|
|
139
172
|
// ---------------------------------------------------------------------------
|
|
140
173
|
|
|
141
|
-
// runViaApi(pick, mode, angle, target, env, timeoutMs?, abortSignal?)
|
|
174
|
+
// runViaApi(pick, mode, angle, target, env, timeoutMs?, abortSignal?, opts?)
|
|
142
175
|
// Returns { status: 'ok', raw, model } or { status: 'failed', error, model }.
|
|
143
|
-
|
|
176
|
+
//
|
|
177
|
+
// opts.cycleSummary (string|null): when set on Anthropic calls, lands in a
|
|
178
|
+
// trailing ephemeral content block AFTER the cacheable user block so it
|
|
179
|
+
// never busts the cache prefix. Ignored for non-Anthropic providers.
|
|
180
|
+
export async function runViaApi(pick, mode, angle, target, env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, abortSignal = null, opts = {}) {
|
|
144
181
|
const fb = pick.apiFallback;
|
|
145
182
|
if (!fb) return { status: 'failed', error: 'no API fallback configured', model: '' };
|
|
146
183
|
|
|
@@ -149,6 +186,7 @@ export async function runViaApi(pick, mode, angle, target, env = process.env, ti
|
|
|
149
186
|
|
|
150
187
|
const { system, format } = getTemplate(mode, angle);
|
|
151
188
|
const user = `${format}\n\n## Target\n\n${target}`;
|
|
189
|
+
const cycleSummary = opts.cycleSummary ?? null;
|
|
152
190
|
|
|
153
191
|
// Combine caller abort signal with our per-call timeout signal.
|
|
154
192
|
const timeoutSig = AbortSignal.timeout(timeoutMs);
|
|
@@ -164,7 +202,7 @@ export async function runViaApi(pick, mode, angle, target, env = process.env, ti
|
|
|
164
202
|
} else if (fb.provider === 'google') {
|
|
165
203
|
req = buildGemini(system, user, fb.model, key, timeoutMs, fb.endpoint);
|
|
166
204
|
} else if (fb.provider === 'anthropic') {
|
|
167
|
-
req = buildAnthropic(system, user, fb.model, key, timeoutMs);
|
|
205
|
+
req = buildAnthropic(system, user, fb.model, key, timeoutMs, cycleSummary);
|
|
168
206
|
} else {
|
|
169
207
|
return { status: 'failed', error: `unknown provider: ${fb.provider}`, model: fb.model };
|
|
170
208
|
}
|
package/src/audit-roster.js
CHANGED
|
@@ -12,6 +12,38 @@
|
|
|
12
12
|
// gets filtered as "self."
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { getLatestModel } from './model-refresh.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// v1.5.0 F5 -- audit-rotation v0 schema (schema-only; runtime ships in v1.6.0)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
//
|
|
21
|
+
// We are NOT shipping rotation logic in v1.5.0. We are shipping the schema
|
|
22
|
+
// commitment so callers / future code have a stable contract to encode
|
|
23
|
+
// against. Auto-rotation flips on in v1.6.0 once we have telemetry to
|
|
24
|
+
// decide rotation policy (cost-weighted, win-rate-weighted, round-robin).
|
|
25
|
+
//
|
|
26
|
+
// Contract:
|
|
27
|
+
// - ROTATION_SCHEMA_VERSION = 1: any future schema change bumps this
|
|
28
|
+
// integer; consumers MUST refuse to apply policy from a higher version
|
|
29
|
+
// than they support.
|
|
30
|
+
// - defaultRotationPolicy = 'manual': v0 behavior is "no rotation;
|
|
31
|
+
// caller (or operator) picks the auditor explicitly via pickAuditors
|
|
32
|
+
// `only:` or default-priority strategy." Other policy values reserved
|
|
33
|
+
// for v1.6.0: 'round-robin', 'cost-weighted', 'win-rate-weighted'.
|
|
34
|
+
//
|
|
35
|
+
// Shape (reserved; not consumed yet):
|
|
36
|
+
// {
|
|
37
|
+
// schema: ROTATION_SCHEMA_VERSION,
|
|
38
|
+
// policy: defaultRotationPolicy,
|
|
39
|
+
// window_days: 7, // reserved -- look-back for win-rate
|
|
40
|
+
// min_picks_per_auditor: 1, // reserved -- floor on usage
|
|
41
|
+
// last_rotated: <ISO>, // reserved -- persistence anchor
|
|
42
|
+
// }
|
|
43
|
+
//
|
|
44
|
+
/** @typedef {{ schema: number, policy: string, window_days?: number, min_picks_per_auditor?: number, last_rotated?: string }} RotationPolicy */
|
|
45
|
+
export const ROTATION_SCHEMA_VERSION = 1;
|
|
46
|
+
export const defaultRotationPolicy = 'manual';
|
|
15
47
|
|
|
16
48
|
export const ROSTER = [
|
|
17
49
|
{
|
|
@@ -19,7 +51,22 @@ export const ROSTER = [
|
|
|
19
51
|
family: 'openai',
|
|
20
52
|
model: '',
|
|
21
53
|
name: 'Codex CLI',
|
|
54
|
+
// Prompt-via-stdin path. Proven working 2026-05-18 with codex-cli 0.130.0.
|
|
22
55
|
invoke: 'codex exec --skip-git-repo-check --sandbox read-only -c approval_policy="never" -c mcp_servers.ijfw-memory.enabled=false -',
|
|
56
|
+
// Dedicated review subcommand path. Use when an audit target is a git ref
|
|
57
|
+
// (HEAD~N, branch name, or commit SHA). The -c mcp_servers.ijfw-memory.enabled=false
|
|
58
|
+
// override is LOAD-BEARING: without it, codex review hangs indefinitely on
|
|
59
|
+
// the ijfw_memory_prelude MCP tool autostart (cycle: codex spawns IJFW MCP
|
|
60
|
+
// server, prelude tool waits on a response, IJFW MCP server is itself the
|
|
61
|
+
// child of the codex session). Verified 2026-05-18, codex-cli 0.130.0.
|
|
62
|
+
// {REF} is the substitution token the caller swaps for the base git ref.
|
|
63
|
+
reviewInvoke: 'codex review --base {REF} -c approval_policy="never" -c mcp_servers.ijfw-memory.enabled=false',
|
|
64
|
+
// 8 min default per-auditor budget for review work. codex review against
|
|
65
|
+
// HEAD~5 with MCP disabled completed in ~75s during S7 reproduction;
|
|
66
|
+
// larger diffs and reasoning-heavy targets need headroom. The existing
|
|
67
|
+
// PROVIDER_TIMEOUT_MS['codex'] in cross-orchestrator.js is 120s (2 min),
|
|
68
|
+
// which is fine for exec-mode quick prompts but too tight for review.
|
|
69
|
+
timeoutMs: 8 * 60 * 1000,
|
|
23
70
|
note: 'Different training lineage; fast on review tasks. The - flag reads prompt from stdin. --skip-git-repo-check bypasses the trusted-directory gate added in codex-cli 0.118.0. --sandbox read-only blocks file WRITES on the host (verified Codex 0.122.0: `echo > /tmp/x` returns `operation not permitted`); it does NOT block shell exec or subprocess launching -- a `read-only` sandbox can still run `ls`, `curl`, or `gemini`. The defense against codex going meta and shelling out to other auditors is the prompt-layer "Operating constraints" block in cross-dispatcher.js buildRequest, not the sandbox flag. The model layer additionally refuses to read explicitly-secret files like ~/.ssh/id_rsa or ~/.codex/auth.json even when prompt-injected to do so. The visibility surface in cross-orchestrator-cli.js cmdCross catches any residual silent failure. approval_policy="never" auto-approves without an interactive prompt. mcp_servers.ijfw-memory.enabled=false disables IJFW MCP for this session because Codex in `codex exec` mode under a non-bypass sandbox auto-cancels MCP tool calls -- the cancellation noise wastes tokens and the audit does not need IJFW memory recall (the brief contains the full target inline).',
|
|
24
71
|
// CODEX_SESSION_ID is set by codex itself when running INSIDE a codex
|
|
25
72
|
// session; CODEX_HOME is a config-path env var that's set whenever codex
|
|
@@ -28,7 +75,11 @@ export const ROSTER = [
|
|
|
28
75
|
// installed but where the caller is something else (Claude Code, Cursor,
|
|
29
76
|
// etc.). Surface noted by carrmjw during the qwen roster review (#11).
|
|
30
77
|
detect: (env) => Boolean(env.CODEX_SESSION_ID) || /codex/i.test(env._ || ''),
|
|
31
|
-
|
|
78
|
+
// model is resolved at call-time via model-refresh.js (24h-cached probe of
|
|
79
|
+
// /v1/models). Hardcoded value below is the offline fallback only.
|
|
80
|
+
get apiFallback() {
|
|
81
|
+
return { provider: 'openai', model: getLatestModel('openai'), authEnv: 'OPENAI_API_KEY', endpoint: 'https://api.openai.com/v1/chat/completions' };
|
|
82
|
+
},
|
|
32
83
|
},
|
|
33
84
|
{
|
|
34
85
|
id: 'gemini',
|
|
@@ -38,7 +89,10 @@ export const ROSTER = [
|
|
|
38
89
|
invoke: 'gemini',
|
|
39
90
|
note: 'Strong on security + architectural patterns. Auto-detects piped stdin for headless mode.',
|
|
40
91
|
detect: (env) => Boolean(env.GEMINI_CLI || env.GOOGLE_CLOUD_PROJECT_GEMINI) || /gemini-cli/i.test(env._ || ''),
|
|
41
|
-
|
|
92
|
+
// model is resolved at call-time via model-refresh.js (24h-cached probe).
|
|
93
|
+
get apiFallback() {
|
|
94
|
+
return { provider: 'google', model: getLatestModel('google'), authEnv: 'GEMINI_API_KEY', endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent' };
|
|
95
|
+
},
|
|
42
96
|
},
|
|
43
97
|
{
|
|
44
98
|
id: 'qwen',
|
|
@@ -108,7 +162,10 @@ export const ROSTER = [
|
|
|
108
162
|
invoke: 'claude -p',
|
|
109
163
|
note: 'Anthropic; useful when you want a second Claude pass in a fresh session.',
|
|
110
164
|
detect: (env) => Boolean(env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT || env.CLAUDE_PLUGIN_ROOT),
|
|
111
|
-
|
|
165
|
+
// model is resolved at call-time via model-refresh.js (24h-cached probe).
|
|
166
|
+
get apiFallback() {
|
|
167
|
+
return { provider: 'anthropic', model: getLatestModel('anthropic'), authEnv: 'ANTHROPIC_API_KEY', endpoint: 'https://api.anthropic.com/v1/messages' };
|
|
168
|
+
},
|
|
112
169
|
},
|
|
113
170
|
];
|
|
114
171
|
|
|
@@ -122,9 +179,27 @@ export function detectSelf(env = process.env) {
|
|
|
122
179
|
|
|
123
180
|
// Probe whether the auditor's CLI is on PATH. Cached per process.
|
|
124
181
|
// Exported so tests can prime the cache for deterministic behavior.
|
|
182
|
+
//
|
|
183
|
+
// v1.5.0 audit-LOW-trident-L1: cache entries now carry a timestamp and
|
|
184
|
+
// expire after 5 minutes. A long-running orchestrator session that installs
|
|
185
|
+
// an auditor mid-session (e.g. `npm install -g @google/gemini-cli`) was
|
|
186
|
+
// otherwise stuck with the stale "not installed" verdict for the rest of
|
|
187
|
+
// the process lifetime. 5min is comfortably longer than a typical Trident
|
|
188
|
+
// fan-out (which probes every auditor up front) but short enough that a
|
|
189
|
+
// mid-session install is detected on the next round.
|
|
125
190
|
export const _installedCache = new Map();
|
|
191
|
+
const INSTALLED_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
126
192
|
export function isInstalled(id) {
|
|
127
|
-
|
|
193
|
+
const cached = _installedCache.get(id);
|
|
194
|
+
if (cached !== undefined) {
|
|
195
|
+
// Legacy entries (primed by tests as a raw boolean) are honoured
|
|
196
|
+
// forever — tests rely on that contract. New entries carry {value, ts}.
|
|
197
|
+
if (typeof cached === 'boolean') return cached;
|
|
198
|
+
if (cached && typeof cached === 'object' && cached.ts + INSTALLED_CACHE_TTL_MS > Date.now()) {
|
|
199
|
+
return cached.value;
|
|
200
|
+
}
|
|
201
|
+
// expired — fall through to re-probe
|
|
202
|
+
}
|
|
128
203
|
const entry = ROSTER.find(e => e.id === id);
|
|
129
204
|
if (!entry) return false;
|
|
130
205
|
// First word of invoke is the binary; the rest are args.
|
|
@@ -133,7 +208,7 @@ export function isInstalled(id) {
|
|
|
133
208
|
// works reliably across macOS + Linux. spawnSync exit code = 0 → present.
|
|
134
209
|
const r = spawnSync('bash', ['-lc', `command -v ${JSON.stringify(bin)} >/dev/null 2>&1`], { timeout: 2000 });
|
|
135
210
|
const installed = r.status === 0;
|
|
136
|
-
_installedCache.set(id, installed);
|
|
211
|
+
_installedCache.set(id, { value: installed, ts: Date.now() });
|
|
137
212
|
return installed;
|
|
138
213
|
}
|
|
139
214
|
|