@ijfw/memory-server 1.4.3 → 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 +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -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 +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- 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 +136 -0
- 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 +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- 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 +113 -12
- 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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* termination.js — Composable termination conditions for orchestrator loops
|
|
3
|
+
* (v1.5.0 audit-MED-work-M3, AutoGen-style API).
|
|
4
|
+
*
|
|
5
|
+
* Every condition is a function `(iter: number, state: object) => boolean`
|
|
6
|
+
* returning TRUE iff the loop should stop. Conditions compose via `or` (any
|
|
7
|
+
* fires) and `and` (all fire). The `runtime-loop.js` wrappers accept an
|
|
8
|
+
* optional `termination:` predicate; default behaviour is `MaxAttempts(N)`.
|
|
9
|
+
*
|
|
10
|
+
* State shape passed by the loop caller — fields are read defensively so
|
|
11
|
+
* conditions can be combined without worrying about which is "responsible"
|
|
12
|
+
* for which field:
|
|
13
|
+
* {
|
|
14
|
+
* startTimestamp?: number, // unix ms (or seconds — see WallClockTimeout)
|
|
15
|
+
* tokensUsed?: number, // running total
|
|
16
|
+
* findings?: Array<{ severity: 'HIGH'|'MEDIUM'|'LOW'|'INFO' }>,
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Atomic conditions
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Stop after `maxAttempts` iterations (iter is 0-indexed; condition fires
|
|
26
|
+
* when `iter + 1 >= maxAttempts`, i.e. after the Nth attempt completes).
|
|
27
|
+
*
|
|
28
|
+
* @param {number} maxAttempts positive integer
|
|
29
|
+
* @returns {(iter: number, state?: object) => boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function MaxAttempts(maxAttempts) {
|
|
32
|
+
if (!Number.isFinite(maxAttempts) || maxAttempts < 1) {
|
|
33
|
+
throw new TypeError(`MaxAttempts: maxAttempts must be >= 1, got ${maxAttempts}`);
|
|
34
|
+
}
|
|
35
|
+
return (iter /* , _state */) => iter + 1 >= maxAttempts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stop after `budgetMs` milliseconds of wall-clock have elapsed since
|
|
40
|
+
* `state.startTimestamp` (which the caller must initialise — usually
|
|
41
|
+
* `Date.now()` before the first iteration).
|
|
42
|
+
*
|
|
43
|
+
* Defensive: missing/non-number startTimestamp falls back to never-firing
|
|
44
|
+
* (a misconfigured timeout shouldn't accidentally terminate the loop).
|
|
45
|
+
*
|
|
46
|
+
* @param {number} budgetMs
|
|
47
|
+
* @returns {(iter: number, state: object) => boolean}
|
|
48
|
+
*/
|
|
49
|
+
export function WallClockTimeout(budgetMs) {
|
|
50
|
+
if (!Number.isFinite(budgetMs) || budgetMs < 0) {
|
|
51
|
+
throw new TypeError(`WallClockTimeout: budgetMs must be >= 0, got ${budgetMs}`);
|
|
52
|
+
}
|
|
53
|
+
return (_iter, state) => {
|
|
54
|
+
const start = state && typeof state.startTimestamp === 'number' ? state.startTimestamp : null;
|
|
55
|
+
if (start === null) return false;
|
|
56
|
+
return Date.now() - start >= budgetMs;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stop once `state.tokensUsed` reaches or exceeds `maxTokens`. Defensive:
|
|
62
|
+
* non-number tokensUsed never fires (so callers can opt in without poisoning
|
|
63
|
+
* loops that don't track tokens).
|
|
64
|
+
*
|
|
65
|
+
* @param {number} maxTokens
|
|
66
|
+
* @returns {(iter: number, state: object) => boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function TokenBudget(maxTokens) {
|
|
69
|
+
if (!Number.isFinite(maxTokens) || maxTokens < 0) {
|
|
70
|
+
throw new TypeError(`TokenBudget: maxTokens must be >= 0, got ${maxTokens}`);
|
|
71
|
+
}
|
|
72
|
+
return (_iter, state) => {
|
|
73
|
+
const used = state && typeof state.tokensUsed === 'number' ? state.tokensUsed : null;
|
|
74
|
+
if (used === null) return false;
|
|
75
|
+
return used >= maxTokens;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stop when `state.findings` contains at least one finding of `severity` or
|
|
81
|
+
* higher. Severity ladder (high → low): HIGH > MEDIUM > LOW > INFO.
|
|
82
|
+
*
|
|
83
|
+
* @param {'HIGH'|'MEDIUM'|'LOW'|'INFO'} severity
|
|
84
|
+
* @returns {(iter: number, state: object) => boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function FindingSeverity(severity) {
|
|
87
|
+
const rank = { HIGH: 3, MEDIUM: 2, LOW: 1, INFO: 0 };
|
|
88
|
+
const threshold = rank[severity];
|
|
89
|
+
if (threshold === undefined) {
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
`FindingSeverity: severity must be one of HIGH|MEDIUM|LOW|INFO, got ${severity}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return (_iter, state) => {
|
|
95
|
+
const findings = state && Array.isArray(state.findings) ? state.findings : null;
|
|
96
|
+
if (!findings) return false;
|
|
97
|
+
return findings.some((f) => {
|
|
98
|
+
const sev = f && typeof f.severity === 'string' ? f.severity.toUpperCase() : null;
|
|
99
|
+
return sev !== null && (rank[sev] ?? -1) >= threshold;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Composition combinators
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compose: stop iff ANY of the given conditions stop. Mirrors AutoGen's `|`
|
|
110
|
+
* operator on termination conditions.
|
|
111
|
+
*
|
|
112
|
+
* @param {...((iter: number, state: object) => boolean)} conds
|
|
113
|
+
* @returns {(iter: number, state: object) => boolean}
|
|
114
|
+
*/
|
|
115
|
+
export function or(...conds) {
|
|
116
|
+
return (iter, state) => conds.some((c) => c(iter, state));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compose: stop iff ALL of the given conditions stop. Mirrors AutoGen's `&`
|
|
121
|
+
* operator on termination conditions.
|
|
122
|
+
*
|
|
123
|
+
* @param {...((iter: number, state: object) => boolean)} conds
|
|
124
|
+
* @returns {(iter: number, state: object) => boolean}
|
|
125
|
+
*/
|
|
126
|
+
export function and(...conds) {
|
|
127
|
+
// Empty-and is true (trivially satisfied), but we treat the empty-arg case
|
|
128
|
+
// as "never stop" — explicit-is-better-than-implicit.
|
|
129
|
+
if (conds.length === 0) return () => false;
|
|
130
|
+
return (iter, state) => conds.every((c) => c(iter, state));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Negation — stop iff `cond` does NOT stop. Rarely useful alone; included
|
|
135
|
+
* for parity with the boolean algebra.
|
|
136
|
+
*
|
|
137
|
+
* @param {(iter: number, state: object) => boolean} cond
|
|
138
|
+
* @returns {(iter: number, state: object) => boolean}
|
|
139
|
+
*/
|
|
140
|
+
export function not(cond) {
|
|
141
|
+
return (iter, state) => !cond(iter, state);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Helpers for callers
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Default termination used by `runtime-loop.js` callers that didn't pass an
|
|
150
|
+
* explicit predicate. Currently MaxAttempts(REVIEW_MAX_ITERATIONS) — matches
|
|
151
|
+
* the v1.4.4 N3 behaviour (re-review caps at 3 iterations).
|
|
152
|
+
*
|
|
153
|
+
* Caller is free to pass their own predicate via the `termination:` arg.
|
|
154
|
+
*
|
|
155
|
+
* @param {number} [maxAttempts=3]
|
|
156
|
+
* @returns {(iter: number, state?: object) => boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function defaultTermination(maxAttempts = 3) {
|
|
159
|
+
return MaxAttempts(maxAttempts);
|
|
160
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verification-gate.js — Iron-Law completion lint: detects completion
|
|
3
|
+
* claims in a message that lack fresh verification evidence (a Bash
|
|
4
|
+
* test/build call in the same message).
|
|
5
|
+
*
|
|
6
|
+
* Gate enforcement: callers receive { ok, violation, claim, enforce }.
|
|
7
|
+
* When `enforce: 'strict'` (default in post-done-runner.js as of W12-F/F4),
|
|
8
|
+
* the caller MUST refuse to advance on ok=false. Use `enforceVerificationGate`
|
|
9
|
+
* (default strict) to get a thrown `VerificationGateViolation` on failure;
|
|
10
|
+
* use the lower-level `checkVerificationGate` for advisory-only callers
|
|
11
|
+
* that prefer to inspect the result themselves.
|
|
12
|
+
*
|
|
13
|
+
* Violations are persisted to .ijfw/memory/verification-violations.jsonl
|
|
14
|
+
* so the memory-feedback system (v1.4.1 B10) can pattern-detect over time.
|
|
15
|
+
*
|
|
16
|
+
* Landed in W10-A2 (v1.4.4 — N5). Promoted to strict-by-default in
|
|
17
|
+
* W12-F/F4 (v1.5.0-major — RT2-H1).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Error subclass — thrown by enforceVerificationGate in strict mode.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by `enforceVerificationGate` when the gate fails and strict mode
|
|
29
|
+
* is in effect (default). Carries the structured violation alongside the
|
|
30
|
+
* Error contract so callers can branch on `instanceof VerificationGateViolation`.
|
|
31
|
+
*/
|
|
32
|
+
export class VerificationGateViolation extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ violation: string, claim: string }} outcome
|
|
35
|
+
* The failure result returned by `checkVerificationGate`.
|
|
36
|
+
*/
|
|
37
|
+
constructor(outcome) {
|
|
38
|
+
const reason = outcome && outcome.violation
|
|
39
|
+
? String(outcome.violation)
|
|
40
|
+
: 'Verification gate violation';
|
|
41
|
+
super(reason);
|
|
42
|
+
this.name = 'VerificationGateViolation';
|
|
43
|
+
this.violation = reason;
|
|
44
|
+
this.claim = outcome && outcome.claim ? String(outcome.claim) : '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Detection patterns
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// r13-M-01 + r13-M-04: dropped bare `complete`, lowercase `done`, AND lowercase
|
|
53
|
+
// `pass(?:es)?` — all three fired falsely on common neutral language ("not yet
|
|
54
|
+
// complete", "to be done in v1.5", "pass the context"). Detection list is now:
|
|
55
|
+
// protocol literal `DONE`, `completed`/`shipped` (deliberate completion verbs),
|
|
56
|
+
// uppercase `PASS` (verdict literal), `✅` emoji, and explicit completion phrases
|
|
57
|
+
// ("all tests pass" / "build succeeded" / "deployed" / "ready to ship").
|
|
58
|
+
//
|
|
59
|
+
// v1.5.0 audit-MED-work-M8: documented the rationale + introduced a SECOND
|
|
60
|
+
// "low-confidence" tier (LOW_CONFIDENCE_PATTERNS) intended for advise-mode-
|
|
61
|
+
// only callers. The strict-by-default gate (enforceVerificationGate) keeps
|
|
62
|
+
// using COMPLETION_PATTERNS so the existing iron-law stays unchanged; tools
|
|
63
|
+
// that want to surface "you might have claimed completion" without blocking
|
|
64
|
+
// can call `checkVerificationGateLowConfidence`. See CHANGELOG v1.5.0 for
|
|
65
|
+
// the policy entry.
|
|
66
|
+
const COMPLETION_PATTERNS = [
|
|
67
|
+
/\b(?:DONE|completed|shipped|PASS)\b/,
|
|
68
|
+
/✅/,
|
|
69
|
+
/\b(?:all tests pass|build succeeded|deployed|ready to ship)\b/i,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Low-confidence completion signals — words that USED to be in
|
|
74
|
+
* COMPLETION_PATTERNS but were removed in r13-M-01 / r13-M-04 because they
|
|
75
|
+
* fired on neutral language. These should NEVER block (strict-mode default
|
|
76
|
+
* uses COMPLETION_PATTERNS only), but advise-mode callers can use them to
|
|
77
|
+
* nudge an agent that's drifting toward a claim without evidence.
|
|
78
|
+
*/
|
|
79
|
+
export const LOW_CONFIDENCE_PATTERNS = [
|
|
80
|
+
/\b(?:done|complete|passes|works|finished)\b/i,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Bash tool calls that count as fresh verification evidence.
|
|
84
|
+
//
|
|
85
|
+
// v1.5.1 H1.1 (audit HIGH-S2): the bare `build` substring let non-build
|
|
86
|
+
// commands like `Bash("ls build/")` clear the Iron Law without running tests.
|
|
87
|
+
//
|
|
88
|
+
// v1.5.1 H1.1-followup (Trident r18 finding): the first cut used `[\s;&|]`
|
|
89
|
+
// as a command-start marker, but plain whitespace inside arguments still let
|
|
90
|
+
// `echo npm test` and `printf 'npm run build'` satisfy the gate. The fix is
|
|
91
|
+
// structural, not regex-stretching: split the bash command into chain-
|
|
92
|
+
// segments on real shell separators (`;`, `&&`, `||`, `|`) and require a
|
|
93
|
+
// verify verb at the START of at least one segment (after an optional
|
|
94
|
+
// env-var prefix like `NODE_ENV=production`).
|
|
95
|
+
/* eslint-disable security/detect-unsafe-regex --
|
|
96
|
+
* Matches verify-cmd strings from developer-authored plan/spec files (not
|
|
97
|
+
* network input). Fixed-suffix alternations + bounded env-var prefix loop
|
|
98
|
+
* are not backtrack-exploitable.
|
|
99
|
+
*/
|
|
100
|
+
const VERIFY_VERB_RE =
|
|
101
|
+
/^(?:[A-Z_][A-Z0-9_]*=\S+\s+)*(?:npm test|node --test|cargo test|pytest|preflight|ijfw preflight|npm run build|yarn build|pnpm build|bun build|cargo build|tsc --build|tsc -b|make(?:\s|$))/i;
|
|
102
|
+
/* eslint-enable security/detect-unsafe-regex */
|
|
103
|
+
|
|
104
|
+
const SHELL_SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||;|\|)\s*/;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return true iff `command` runs a recognized test/build verb as the head of
|
|
108
|
+
* at least one chain-segment. `echo npm test` does NOT match (echo is the
|
|
109
|
+
* head); `cd foo && npm test` DOES (the second segment is `npm test`).
|
|
110
|
+
* Exported for unit-tests of the bash-segment splitter.
|
|
111
|
+
*/
|
|
112
|
+
export function isVerificationCommand(command) {
|
|
113
|
+
if (typeof command !== 'string' || !command) return false;
|
|
114
|
+
const segments = command.trim().split(SHELL_SEGMENT_SPLIT_RE);
|
|
115
|
+
for (const seg of segments) {
|
|
116
|
+
if (VERIFY_VERB_RE.test(seg.trim())) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Core gate
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check whether a message that contains a completion claim also has fresh
|
|
127
|
+
* verification evidence (a Bash tool call running tests/build).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} message Full text of the agent message.
|
|
130
|
+
* @param {Array<{tool: string, input?: {command?: string}}>} toolCallsInMessage
|
|
131
|
+
* Tool calls that appeared in the same message turn.
|
|
132
|
+
*
|
|
133
|
+
* @returns {{ ok: true } | { ok: false, violation: string, claim: string }}
|
|
134
|
+
*/
|
|
135
|
+
export function checkVerificationGate(message, toolCallsInMessage) {
|
|
136
|
+
const claims = COMPLETION_PATTERNS.flatMap((p) => message.match(p) ?? []);
|
|
137
|
+
if (claims.length === 0) return { ok: true };
|
|
138
|
+
|
|
139
|
+
const verificationCalls = toolCallsInMessage.filter(
|
|
140
|
+
(t) => t.tool === 'Bash' && isVerificationCommand(t.input?.command ?? ''),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (verificationCalls.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
violation: `Completion claim "${claims[0]}" without fresh verification in same message`,
|
|
147
|
+
claim: claims[0],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { ok: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Low-confidence advisory variant of `checkVerificationGate`. Uses the
|
|
156
|
+
* LOW_CONFIDENCE_PATTERNS list (lowercase `done`/`complete`/etc.) so callers
|
|
157
|
+
* can detect drift toward completion claims that the strict gate (rightly)
|
|
158
|
+
* ignores. ADVISORY ONLY — never wired into the iron-law enforcement path.
|
|
159
|
+
*
|
|
160
|
+
* Returns the same shape as `checkVerificationGate`. Skipping evidence here
|
|
161
|
+
* is NOT a violation in the strict-enforcement sense; callers decide what
|
|
162
|
+
* to do (typically: surface as INFO finding to the operator).
|
|
163
|
+
*
|
|
164
|
+
* @param {string} message
|
|
165
|
+
* @param {Array<{tool: string, input?: {command?: string}}>} toolCallsInMessage
|
|
166
|
+
* @returns {{ ok: true } | { ok: false, violation: string, claim: string, lowConfidence: true }}
|
|
167
|
+
*/
|
|
168
|
+
export function checkVerificationGateLowConfidence(message, toolCallsInMessage) {
|
|
169
|
+
const claims = LOW_CONFIDENCE_PATTERNS.flatMap((p) => message.match(p) ?? []);
|
|
170
|
+
if (claims.length === 0) return { ok: true };
|
|
171
|
+
|
|
172
|
+
const verificationCalls = (Array.isArray(toolCallsInMessage) ? toolCallsInMessage : []).filter(
|
|
173
|
+
(t) => t.tool === 'Bash' && isVerificationCommand(t.input?.command ?? ''),
|
|
174
|
+
);
|
|
175
|
+
if (verificationCalls.length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
violation: `Low-confidence completion signal "${claims[0]}" without fresh verification (advisory)`,
|
|
179
|
+
claim: claims[0],
|
|
180
|
+
lowConfidence: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { ok: true };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Strict enforcement wrapper (W12-F/F4 — RT2-H1)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Enforce the verification gate. By default this is strict: a failed gate
|
|
192
|
+
* throws `VerificationGateViolation`. Pass `{ strict: false }` for advisory
|
|
193
|
+
* behavior (returns the same shape as `checkVerificationGate`).
|
|
194
|
+
*
|
|
195
|
+
* @param {string} messageText
|
|
196
|
+
* @param {Array<{tool: string, input?: {command?: string}}>} toolCalls
|
|
197
|
+
* @param {{ strict?: boolean }} [opts]
|
|
198
|
+
* @returns {{ ok: true } | { ok: false, violation: string, claim: string }}
|
|
199
|
+
* @throws {VerificationGateViolation} when strict and gate fails.
|
|
200
|
+
*/
|
|
201
|
+
export function enforceVerificationGate(messageText, toolCalls, opts = {}) {
|
|
202
|
+
const strict = opts.strict !== false; // default strict
|
|
203
|
+
const outcome = checkVerificationGate(
|
|
204
|
+
typeof messageText === 'string' ? messageText : '',
|
|
205
|
+
Array.isArray(toolCalls) ? toolCalls : [],
|
|
206
|
+
);
|
|
207
|
+
if (!outcome.ok && strict) {
|
|
208
|
+
throw new VerificationGateViolation(outcome);
|
|
209
|
+
}
|
|
210
|
+
return outcome;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Violation recorder
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Set of target paths whose write failure has already been logged to stderr
|
|
219
|
+
* this process. Prevents stderr flooding when the memory dir is unwritable
|
|
220
|
+
* and a high-frequency caller (e.g. a stuck subagent loop) keeps retrying.
|
|
221
|
+
*
|
|
222
|
+
* v1.5.0 audit-H4.3 (HIGH-Rel1): recordViolation used to silently swallow
|
|
223
|
+
* write failures, defeating the gate's whole job (surfacing violations to
|
|
224
|
+
* the memory-feedback system). The fix:
|
|
225
|
+
* 1. Emit ONE stderr line per failure (with the claim, so the violation
|
|
226
|
+
* isn't fully lost), then
|
|
227
|
+
* 2. Return a result shape so callers can observe success/failure, but
|
|
228
|
+
* 3. NEVER throw — recordViolation remains advisory in posture.
|
|
229
|
+
* Failures that used to be invisible are now AUDIBLE (stderr) +
|
|
230
|
+
* OBSERVABLE (return shape), but still non-fatal.
|
|
231
|
+
*/
|
|
232
|
+
const RECORD_FAILURE_LOGGED = new Set();
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Append a violation record to .ijfw/memory/verification-violations.jsonl.
|
|
236
|
+
* Auto-creates parent directories.
|
|
237
|
+
*
|
|
238
|
+
* Advisory in posture (never throws), but no longer silent: write failures
|
|
239
|
+
* are logged to stderr (once per target-path, dedup'd via a module-level
|
|
240
|
+
* Set) and returned in the result shape so callers can observe them.
|
|
241
|
+
*
|
|
242
|
+
* @param {{ violation: string, claim: string, [key: string]: unknown }} violation
|
|
243
|
+
* @param {string} projectRoot Absolute path to the project root.
|
|
244
|
+
* @returns {Promise<{ ok: true, path: string } | { ok: false, path: string, error: string }>}
|
|
245
|
+
*/
|
|
246
|
+
export async function recordViolation(violation, projectRoot) {
|
|
247
|
+
const file = join(projectRoot, '.ijfw', 'memory', 'verification-violations.jsonl');
|
|
248
|
+
try {
|
|
249
|
+
await mkdir(dirname(file), { recursive: true });
|
|
250
|
+
await appendFile(
|
|
251
|
+
file,
|
|
252
|
+
JSON.stringify({ ...violation, recorded_at: new Date().toISOString() }) + '\n',
|
|
253
|
+
);
|
|
254
|
+
return { ok: true, path: file };
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const errMsg = err && err.message ? String(err.message) : String(err);
|
|
257
|
+
// Dedup: only one stderr line per target path per process, to avoid
|
|
258
|
+
// flooding when the memory dir is unwritable for a long-running loop.
|
|
259
|
+
if (!RECORD_FAILURE_LOGGED.has(file)) {
|
|
260
|
+
RECORD_FAILURE_LOGGED.add(file);
|
|
261
|
+
const claim = violation && violation.claim ? String(violation.claim) : '<unknown>';
|
|
262
|
+
// One-line stderr log so the violation isn't fully lost. Includes the
|
|
263
|
+
// claim string so a downstream operator can correlate.
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
`[verification-gate] recordViolation write failed path=${file} claim=${JSON.stringify(claim)} err=${errMsg}\n`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return { ok: false, path: file, error: errMsg };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Reset the recordViolation failure-dedup Set. Test-only helper so each
|
|
274
|
+
* test can assert "one stderr line per failure" independently of other
|
|
275
|
+
* tests in the same process.
|
|
276
|
+
*
|
|
277
|
+
* @internal
|
|
278
|
+
*/
|
|
279
|
+
export function _resetRecordViolationDedup() {
|
|
280
|
+
RECORD_FAILURE_LOGGED.clear();
|
|
281
|
+
}
|