@ijfw/memory-server 1.5.0 → 1.5.3
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/package.json +8 -4
- package/src/brain/budget-guard.js +86 -0
- package/src/brain/citation-resolver.js +41 -0
- package/src/brain/context-injection.js +69 -0
- package/src/brain/discovery.js +83 -0
- package/src/brain/dream-pipeline.js +324 -0
- package/src/brain/dump-ingest.js +88 -0
- package/src/brain/entity-collapse.js +28 -0
- package/src/brain/export.js +112 -0
- package/src/brain/extractors/index.js +24 -0
- package/src/brain/extractors/markdown.js +27 -0
- package/src/brain/extractors/pdf.js +31 -0
- package/src/brain/extractors/transcript.js +38 -0
- package/src/brain/first-run-scan.js +61 -0
- package/src/brain/index.js +1 -0
- package/src/brain/layout-sentinel.js +29 -0
- package/src/brain/migrate-facts-internal-once.js +87 -0
- package/src/brain/path-guard.js +103 -0
- package/src/brain/paths.js +26 -0
- package/src/brain/promotion-suggester.js +41 -0
- package/src/brain/stub-detector.js +33 -0
- package/src/brain/tiered-llm.js +83 -0
- package/src/brain/wiki-compiler.js +144 -0
- package/src/brain/wiki-sentinels.js +45 -0
- package/src/brain/wiki-templates.js +94 -0
- package/src/cross-orchestrator-cli.js +336 -150
- package/src/cross-orchestrator.js +52 -3
- package/src/dashboard-server.js +1 -1
- package/src/dispatch/extension.js +1 -1
- package/src/dream/runner.mjs +21 -0
- package/src/extension-registry.js +2 -2
- package/src/handlers/brain-handler.js +319 -0
- package/src/hardware-signer.js +4 -2
- package/src/lib/ui-review-runner.js +48 -7
- package/src/memory/auto-linker.js +121 -2
- package/src/memory/benchmark.js +4 -3
- package/src/memory/layout-migrations/001-visible-layer.js +131 -0
- package/src/memory/layout-migrations/index.js +50 -0
- package/src/memory/migration-runner.js +37 -3
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +65 -2
- package/src/memory/reader.js +2 -1
- package/src/memory/search.js +190 -41
- package/src/memory/temporal.js +40 -1
- package/src/orchestrator/agents-md-blackboard.js +114 -1
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/discipline-selector.js +276 -0
- package/src/orchestrator/merge-block-aware.js +15 -5
- package/src/orchestrator/post-done-runner.js +36 -8
- package/src/orchestrator/state-sdk.js +216 -10
- package/src/orchestrator/subagent-telemetry.js +19 -0
- package/src/orchestrator/wave-state.js +38 -0
- package/src/override-resolver.js +5 -3
- package/src/recovery/code-fixer.js +311 -6
- package/src/runtime-mediator.js +0 -1
- package/src/server.js +486 -132
- package/src/swarm-config.js +30 -22
- package/src/team/domain-templates/business.json +4 -1
- package/src/team/domain-templates/research.json +4 -1
- package/src/team/generator.js +162 -0
- package/src/update-apply.js +1 -1
- package/src/dashboard-charts.js +0 -239
- package/src/orchestrator/runtime-loop.js +0 -430
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*
|
|
35
35
|
* RESERVED BLOCK NAMES
|
|
36
36
|
* Matches the shell script verbatim:
|
|
37
|
-
* MEMORY | ROUTING | AGENTS | BLACKBOARD | FRONTMATTER
|
|
37
|
+
* MEMORY | ROUTING | AGENTS | BLACKBOARD | FRONTMATTER | DISCIPLINE
|
|
38
38
|
*
|
|
39
39
|
* ESM, Node ≥18, zero new prod deps.
|
|
40
40
|
*/
|
|
@@ -60,7 +60,7 @@ import { writeAtomic } from '../lib/atomic-io.js';
|
|
|
60
60
|
* name yields `ERR_BAD_BLOCK` (the same exit-2 the shell script returns).
|
|
61
61
|
*/
|
|
62
62
|
export const RESERVED_BLOCKS = Object.freeze([
|
|
63
|
-
'MEMORY', 'ROUTING', 'AGENTS', 'BLACKBOARD', 'FRONTMATTER',
|
|
63
|
+
'MEMORY', 'ROUTING', 'AGENTS', 'BLACKBOARD', 'FRONTMATTER', 'DISCIPLINE',
|
|
64
64
|
]);
|
|
65
65
|
|
|
66
66
|
const RESERVED_BLOCK_SET = new Set(RESERVED_BLOCKS);
|
|
@@ -149,7 +149,7 @@ export function mergeBlocks(src, pairs) {
|
|
|
149
149
|
if (typeof block !== 'string' || !RESERVED_BLOCK_SET.has(block)) {
|
|
150
150
|
throw new MergeBlockAwareError(
|
|
151
151
|
'ERR_BAD_BLOCK',
|
|
152
|
-
`mergeBlocks: block
|
|
152
|
+
`mergeBlocks: unknown block ${JSON.stringify(block)} -- reserved set: ${RESERVED_BLOCKS.join(' ')}`,
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
const content = (pair.content === undefined || pair.content === null)
|
|
@@ -310,6 +310,18 @@ export function mergeFile(targetAbsPath, pairs, opts = {}) {
|
|
|
310
310
|
seeded = true;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
const src = readFileSync(abs, 'utf8');
|
|
314
|
+
const next = mergeBlocks(src, pairs);
|
|
315
|
+
|
|
316
|
+
// No-op short-circuit: if content is unchanged and this is not a seed,
|
|
317
|
+
// skip backup rotation and the atomic write entirely. Idempotent calls
|
|
318
|
+
// will no longer accumulate identical-content backups.
|
|
319
|
+
if (!seeded && next === src) {
|
|
320
|
+
return {
|
|
321
|
+
ok: true, path: abs, bytes: Buffer.byteLength(next), seeded: false, noop: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
313
325
|
// Backup + retention (best-effort, defaults on). `opts.backups === false`
|
|
314
326
|
// suppresses (used by some tests to keep tmp clean).
|
|
315
327
|
let backup;
|
|
@@ -318,8 +330,6 @@ export function mergeFile(targetAbsPath, pairs, opts = {}) {
|
|
|
318
330
|
if (rot.taken && rot.path) backup = rot.path;
|
|
319
331
|
}
|
|
320
332
|
|
|
321
|
-
const src = readFileSync(abs, 'utf8');
|
|
322
|
-
const next = mergeBlocks(src, pairs);
|
|
323
333
|
const res = writeAtomic(abs, next, { mode: 0o644, ensureDir: true });
|
|
324
334
|
return {
|
|
325
335
|
ok: true, path: res.path, bytes: res.bytes, backup, seeded,
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* post-done-runner.js — v1.5.0-major S02:
|
|
2
|
+
* post-done-runner.js — v1.5.0-major S02: post-DONE pipeline primitives.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* WHAT IS LIVE: `runSelfCheck` is the only export on the production path. The
|
|
5
|
+
* live DONE-handler is the `subagent.post-done` state-SDK verb, which calls
|
|
6
|
+
* `runSelfCheck` directly (and fires debug-trident via debug-trident-trigger.js
|
|
7
|
+
* on a self-check failure). The verification gate itself is also enforced live
|
|
8
|
+
* — `state-sdk.js` calls `enforceVerificationGate` directly.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS NOT LIVE: `runPostDone` is a library/test surface — NOT the live
|
|
11
|
+
* DONE-handler. It is a convenience wrapper that bundles reviewTask (v1.4.4 N3
|
|
12
|
+
* two-stage review) + checkVerificationGate (v1.4.4 N5) for direct-import
|
|
13
|
+
* callers and the test path (`test-orchestrator-post-done-runner.js`). The
|
|
14
|
+
* production two-stage spec+quality review happens via agent dispatch
|
|
15
|
+
* (spec-reviewer + quality-reviewer agents), not through this wrapper. Its
|
|
16
|
+
* original S02 caller (`runtime-loop.js`) was never wired; that file is now
|
|
17
|
+
* removed. `runPostDone` is kept for its test surface and for any future
|
|
18
|
+
* caller that wants the two checks bundled — it does not carry production
|
|
19
|
+
* traffic today.
|
|
8
20
|
*
|
|
9
21
|
* v1.5.0 T13: the standalone `ijfw_subagent_post_done` MCP tool was retired and
|
|
10
22
|
* absorbed into the single `ijfw_state` MCP tool as the `subagent.post-done`
|
|
11
23
|
* verb (see STATE-SDK-CONTRACT §7). `runSelfCheck` is re-exported through
|
|
12
|
-
* `state-sdk.js` for that verb
|
|
13
|
-
* direct-import test path (`test-orchestrator-post-done-runner.js`).
|
|
24
|
+
* `state-sdk.js` for that verb.
|
|
14
25
|
*
|
|
15
26
|
* Outcome shape (uniform regardless of branch taken):
|
|
16
27
|
* {
|
|
@@ -46,6 +57,13 @@ import { existsSync } from 'node:fs';
|
|
|
46
57
|
import { execFileSync } from 'node:child_process';
|
|
47
58
|
import { reviewTask } from './review.js';
|
|
48
59
|
import { checkVerificationGate, recordViolation } from './verification-gate.js';
|
|
60
|
+
// debug-trident (T29) is wired on the LIVE path only: `subagent.post-done` in
|
|
61
|
+
// state-sdk.js fires debug-trident fire-and-forget when its self-check gate
|
|
62
|
+
// fails, via `maybeFireDebugTrident` in debug-trident-trigger.js. That is the
|
|
63
|
+
// genuine production caller — codex+gemini are dispatched against the real
|
|
64
|
+
// gate-failure evidence whenever IJFW_DEBUG_TRIDENT is enabled. runPostDone
|
|
65
|
+
// deliberately does NOT invoke debug-trident (the earlier W2.C inline-
|
|
66
|
+
// annotation hook was dead — computed but never returned — and was removed).
|
|
49
67
|
|
|
50
68
|
/**
|
|
51
69
|
* Extract paths claimed in the report. Naive but effective: looks for
|
|
@@ -123,7 +141,17 @@ export function runSelfCheck(reportText, projectRoot) {
|
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
/**
|
|
126
|
-
|
|
144
|
+
* runPostDone — library/test surface. NOT the live DONE-handler.
|
|
145
|
+
*
|
|
146
|
+
* The live subagent-completion path is the `subagent.post-done` state-SDK verb
|
|
147
|
+
* (which runs `runSelfCheck` + fires debug-trident on failure), plus the
|
|
148
|
+
* verification gate enforced directly in `state-sdk.js`; the production
|
|
149
|
+
* two-stage spec+quality review runs via agent dispatch (spec-reviewer +
|
|
150
|
+
* quality-reviewer agents). This wrapper bundles reviewTask (N3) +
|
|
151
|
+
* checkVerificationGate (N5) + runSelfCheck (S09) for direct-import callers
|
|
152
|
+
* and `test-orchestrator-post-done-runner.js`. It carries no production
|
|
153
|
+
* traffic — keep it honest: do not describe it as the live handler.
|
|
154
|
+
*
|
|
127
155
|
* @param {object} params
|
|
128
156
|
* @param {string} params.taskId
|
|
129
157
|
* @param {string} [params.taskSpec]
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
* straight off that contract.
|
|
12
12
|
*
|
|
13
13
|
* ───────────────────────────────────────────────────────────────────────────
|
|
14
|
-
* SCOPE BOUNDARY — T2
|
|
14
|
+
* SCOPE BOUNDARY — T2 built the verb core; three later tasks wrapped it.
|
|
15
|
+
* All three (T3/T4/T5) are now REALIZED — this section is kept as the design
|
|
16
|
+
* record, but the seams below are live, not stubs:
|
|
15
17
|
*
|
|
16
|
-
* T3 (lock hierarchy) —
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the
|
|
20
|
-
*
|
|
18
|
+
* T3 (lock hierarchy) — `_withLocks()` is NOT a pass-through. It acquires
|
|
19
|
+
* real filesystem locks via `withFsLock`, ordering
|
|
20
|
+
* each verb's declared lock-target list through
|
|
21
|
+
* `canonicalLockOrder` (the §3 canonical acquire
|
|
22
|
+
* order) to prevent deadlock, then runs `fn` while
|
|
23
|
+
* the locks are held and releases on completion.
|
|
21
24
|
* T4 (intent/commit) — wraps `_journalBegin()` / `_journalCommit()`.
|
|
22
25
|
* Today they are no-ops; T4 makes them write the
|
|
23
26
|
* write-ahead `intent-journal.jsonl` records and
|
|
@@ -34,12 +37,13 @@
|
|
|
34
37
|
*/
|
|
35
38
|
|
|
36
39
|
import {
|
|
37
|
-
readFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, readdirSync,
|
|
40
|
+
readFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, readdirSync, realpathSync,
|
|
38
41
|
} from 'node:fs';
|
|
39
|
-
import { join, isAbsolute, dirname, basename } from 'node:path';
|
|
42
|
+
import { join, isAbsolute, isAbsolute as pathIsAbsolute, relative as pathRelative, dirname, basename } from 'node:path';
|
|
40
43
|
import { homedir } from 'node:os';
|
|
41
44
|
import { randomUUID, createHash } from 'node:crypto';
|
|
42
45
|
import { gunzipSync } from 'node:zlib';
|
|
46
|
+
import { execFileSync } from 'node:child_process';
|
|
43
47
|
|
|
44
48
|
import { writeAtomic, readSafe } from '../lib/atomic-io.js';
|
|
45
49
|
import { rotateJsonlIfNeeded } from '../lib/jsonl-rotation.js';
|
|
@@ -58,6 +62,17 @@ import {
|
|
|
58
62
|
appendUnderHeldLock as appendEventUnderHeldLock,
|
|
59
63
|
resolveEventLogPath,
|
|
60
64
|
} from './state-events.js';
|
|
65
|
+
// v1.5.1 cleanup C1: S08 incident-driven worktree safety guards. Previously
|
|
66
|
+
// orphan (only importer was the unwired runtime-loop.js). Wired here as
|
|
67
|
+
// preconditions of the LIVE `subagent.dispatch` verb — the genuinely-reachable
|
|
68
|
+
// worktree-isolated spawn path (ijfw_state MCP tool → query → subagent.dispatch).
|
|
69
|
+
// worktree-guards.js has no state-sdk dependency, so a static import is safe.
|
|
70
|
+
import {
|
|
71
|
+
captureSpawnToplevel,
|
|
72
|
+
assertPathWithinToplevel,
|
|
73
|
+
assertNoCwdDrift,
|
|
74
|
+
assertNotProtectedRef,
|
|
75
|
+
} from '../lib/worktree-guards.js';
|
|
61
76
|
|
|
62
77
|
// ---------------------------------------------------------------------------
|
|
63
78
|
// Gate-function indirection (T15 — Model 4 testability seam)
|
|
@@ -704,6 +719,88 @@ function requireStr(value, field) {
|
|
|
704
719
|
|
|
705
720
|
function nowIso() { return new Date().toISOString(); }
|
|
706
721
|
|
|
722
|
+
/**
|
|
723
|
+
* v1.5.1 cleanup C1 — S08 worktree-guard preconditions for `subagent.dispatch`.
|
|
724
|
+
*
|
|
725
|
+
* Runs the three incident-driven guards (worktree-guards.js) against the
|
|
726
|
+
* project root a worktree-isolated subagent is about to be dispatched into:
|
|
727
|
+
* #3099 — assertPathWithinToplevel (abs-path / symlink escape)
|
|
728
|
+
* #3097 — assertNoCwdDrift (cwd drifted off the toplevel)
|
|
729
|
+
* #2924 — assertNotProtectedRef (HEAD on main/master/develop/…)
|
|
730
|
+
*
|
|
731
|
+
* Semantics — `subagent.dispatch` is a brief-COMPOSITION verb (it produces a
|
|
732
|
+
* deterministic dispatch brief; the real spawn happens platform-side). The
|
|
733
|
+
* guards are therefore PRECONDITIONS surfaced on the result, not a hard abort
|
|
734
|
+
* of brief composition:
|
|
735
|
+
* - Project root IS a git repo → all 3 guards run. A failure is reported on
|
|
736
|
+
* `guard.violations[]` and `guard.ok=false`. With IJFW_WORKTREE_GUARD_STRICT=1
|
|
737
|
+
* a violation throws (aborts the dispatch before `_withLocks`).
|
|
738
|
+
* - Project root is NOT a git repo (or guards can't run) → `guard.ok` stays
|
|
739
|
+
* true with `guard.skipped` set; brief composition proceeds. This keeps the
|
|
740
|
+
* verb usable from non-git temp dirs (tests, scratch projects).
|
|
741
|
+
*
|
|
742
|
+
* Only `'worktree'` isolation is guarded — `'shared'` dispatch spawns no
|
|
743
|
+
* separate worktree so containment/drift/protected-ref don't apply.
|
|
744
|
+
*
|
|
745
|
+
* @param {string} projectRoot absolute path the subagent dispatches into
|
|
746
|
+
* @param {'shared'|'worktree'} isolation
|
|
747
|
+
* @returns {{ok:boolean, skipped?:string, violations:string[], branch?:string}}
|
|
748
|
+
* @throws {Error} only when IJFW_WORKTREE_GUARD_STRICT=1 and a guard fails
|
|
749
|
+
*/
|
|
750
|
+
function runWorktreeGuards(projectRoot, isolation) {
|
|
751
|
+
if (isolation !== 'worktree') {
|
|
752
|
+
return { ok: true, skipped: 'shared-isolation', violations: [] };
|
|
753
|
+
}
|
|
754
|
+
// Quiet git-repo pre-check — captureSpawnToplevel would otherwise let git's
|
|
755
|
+
// own "fatal: not a git repository" hit our stderr on non-git roots (common
|
|
756
|
+
// in tests / scratch projects). Suppress git stderr here, then run the real
|
|
757
|
+
// guards only when we know we're inside a work tree.
|
|
758
|
+
try {
|
|
759
|
+
const probe = execFileSync(
|
|
760
|
+
'git', ['rev-parse', '--is-inside-work-tree'],
|
|
761
|
+
{ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
762
|
+
).trim();
|
|
763
|
+
if (probe !== 'true') {
|
|
764
|
+
return { ok: true, skipped: 'not-a-git-repo', violations: [] };
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
// Not a git tree — nothing to contain. Brief composition still proceeds.
|
|
768
|
+
return { ok: true, skipped: 'not-a-git-repo', violations: [] };
|
|
769
|
+
}
|
|
770
|
+
let toplevel;
|
|
771
|
+
try {
|
|
772
|
+
toplevel = captureSpawnToplevel(projectRoot);
|
|
773
|
+
} catch {
|
|
774
|
+
return { ok: true, skipped: 'not-a-git-repo', violations: [] };
|
|
775
|
+
}
|
|
776
|
+
const violations = [];
|
|
777
|
+
let branch;
|
|
778
|
+
try {
|
|
779
|
+
assertPathWithinToplevel(projectRoot, toplevel);
|
|
780
|
+
} catch (e) {
|
|
781
|
+
violations.push(`path-escape: ${e.message}`);
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
assertNoCwdDrift(toplevel, projectRoot);
|
|
785
|
+
} catch (e) {
|
|
786
|
+
violations.push(`cwd-drift: ${e.message}`);
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
branch = assertNotProtectedRef(projectRoot);
|
|
790
|
+
} catch (e) {
|
|
791
|
+
violations.push(`protected-ref: ${e.message}`);
|
|
792
|
+
}
|
|
793
|
+
const ok = violations.length === 0;
|
|
794
|
+
if (!ok && process.env.IJFW_WORKTREE_GUARD_STRICT === '1') {
|
|
795
|
+
throw new Error(
|
|
796
|
+
`state-sdk: subagent.dispatch S08 worktree guard failed — ${violations.join('; ')}`,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return ok
|
|
800
|
+
? { ok: true, violations: [], branch }
|
|
801
|
+
: { ok: false, violations };
|
|
802
|
+
}
|
|
803
|
+
|
|
707
804
|
// ---------------------------------------------------------------------------
|
|
708
805
|
// VERB HANDLERS — one per contract §7 block. Signature: (payload, ctx, env).
|
|
709
806
|
// `env` carries the per-invocation { verbId } so handlers can stamp records.
|
|
@@ -1000,6 +1097,13 @@ const handlers = {
|
|
|
1000
1097
|
const isolation = payload?.isolation === 'shared' ? 'shared' : 'worktree';
|
|
1001
1098
|
const role = typeof payload?.role === 'string' && payload.role.length > 0
|
|
1002
1099
|
? payload.role : null;
|
|
1100
|
+
// v1.5.1 cleanup C1 — S08 worktree-guard preconditions. Runs the three
|
|
1101
|
+
// incident-driven guards BEFORE the wave-state mutation when isolation is
|
|
1102
|
+
// 'worktree'. This is the production caller worktree-guards.js was missing
|
|
1103
|
+
// (its only prior importer was the unwired runtime-loop.js). With
|
|
1104
|
+
// IJFW_WORKTREE_GUARD_STRICT=1 a guard violation throws here, aborting the
|
|
1105
|
+
// dispatch before `_withLocks`; otherwise it is surfaced on the result.
|
|
1106
|
+
const worktreeGuard = runWorktreeGuards(root, isolation);
|
|
1003
1107
|
// Caller-supplied env passthrough (object: name → value). Coerce values to
|
|
1004
1108
|
// strings (env vars are always strings) and drop nullish entries.
|
|
1005
1109
|
const callerEnv = (payload?.env && typeof payload.env === 'object'
|
|
@@ -1055,8 +1159,46 @@ const handlers = {
|
|
|
1055
1159
|
const envLines = Object.keys(inheritedEnv).sort()
|
|
1056
1160
|
.map((k) => ` ${k}=${inheritedEnv[k]}`);
|
|
1057
1161
|
const eventLogPath = resolveEventLogPath(root, waveId, subagentId);
|
|
1058
|
-
|
|
1059
|
-
|
|
1162
|
+
// F5.6: the prior `startsWith(root + '/')` form (a) wired only on POSIX
|
|
1163
|
+
// because the separator is `/`, breaking on Windows where the separator
|
|
1164
|
+
// is `\`, and (b) silently leaked absolute paths into the dispatch brief
|
|
1165
|
+
// on Windows because the startsWith check always returned false. Use
|
|
1166
|
+
// path.relative + isAbsolute so the relative form is computed correctly
|
|
1167
|
+
// cross-platform, and fall back to the absolute path only when the
|
|
1168
|
+
// event log is genuinely outside the repo (rel '..' or different drive).
|
|
1169
|
+
// F-LENS2-13: pre-canonicalize via realpathSync so a Windows 8.3 short-
|
|
1170
|
+
// name (PROGRA~1) or a macOS /tmp -> /private/tmp symlink doesn't make
|
|
1171
|
+
// the path.relative containment check spuriously claim "outside repo".
|
|
1172
|
+
//
|
|
1173
|
+
// Canonicalization MUST be atomic across both sides: realpathSync(root)
|
|
1174
|
+
// can succeed (root exists) while realpathSync(eventLogPath) throws
|
|
1175
|
+
// ENOENT (event log file is about to be created). Independent try/catches
|
|
1176
|
+
// produce ASYMMETRIC results — on macOS /var/folders is a symlink to
|
|
1177
|
+
// /private/var/folders, so canonicalRoot becomes the long form while
|
|
1178
|
+
// canonicalEventLog stays short, path.relative returns a `..`-leading
|
|
1179
|
+
// string, and the containment check spuriously rejects — leaking the
|
|
1180
|
+
// absolute path into the dispatch brief. One try/catch keeps both sides
|
|
1181
|
+
// either canonical or both un-canonical.
|
|
1182
|
+
let canonicalRoot, canonicalEventLog;
|
|
1183
|
+
try {
|
|
1184
|
+
canonicalRoot = realpathSync(root);
|
|
1185
|
+
canonicalEventLog = realpathSync(eventLogPath);
|
|
1186
|
+
} catch {
|
|
1187
|
+
canonicalRoot = root;
|
|
1188
|
+
canonicalEventLog = eventLogPath;
|
|
1189
|
+
}
|
|
1190
|
+
const _rel = pathRelative(canonicalRoot, canonicalEventLog);
|
|
1191
|
+
// F-LENS2-14: when the event log is genuinely outside the repo, the
|
|
1192
|
+
// earlier behaviour leaked the FULL absolute path (often containing
|
|
1193
|
+
// $HOME / $USERPROFILE) into the dispatch brief — which the subagent
|
|
1194
|
+
// sees and may forward to an external LLM provider. Redact to a
|
|
1195
|
+
// pseudo-path so the subagent knows the log exists by basename without
|
|
1196
|
+
// leaking the absolute prefix. The orchestrator still has the real
|
|
1197
|
+
// eventLogPath in its return value for its own I/O.
|
|
1198
|
+
const _basename = (eventLogPath || '').split(/[\\/]/).pop() || 'events.jsonl';
|
|
1199
|
+
const eventLogRel = (_rel === '' || _rel.startsWith('..') || pathIsAbsolute(_rel))
|
|
1200
|
+
? `<external>/${_basename}`
|
|
1201
|
+
: _rel;
|
|
1060
1202
|
const dispatchBrief = [
|
|
1061
1203
|
`# Subagent dispatch — ${subagentId} (wave ${waveId})`,
|
|
1062
1204
|
role ? `Role: ${role}` : null,
|
|
@@ -1078,6 +1220,11 @@ const handlers = {
|
|
|
1078
1220
|
isolation,
|
|
1079
1221
|
inheritedEnv,
|
|
1080
1222
|
eventLogPath,
|
|
1223
|
+
// v1.5.1 cleanup C1 — S08 guard outcome. `{ok:true}` when guards passed
|
|
1224
|
+
// or were skipped (non-git root / shared isolation); `{ok:false,
|
|
1225
|
+
// violations[]}` when a containment/drift/protected-ref hazard was
|
|
1226
|
+
// detected (non-strict mode — the orchestrator should not spawn).
|
|
1227
|
+
worktreeGuard,
|
|
1081
1228
|
};
|
|
1082
1229
|
}, env);
|
|
1083
1230
|
},
|
|
@@ -1127,12 +1274,69 @@ const handlers = {
|
|
|
1127
1274
|
process.stderr.write(`[state-sdk] WARN subagent.post-done gate execution-fail: ${e.message}\n`);
|
|
1128
1275
|
return { ok: true, advisory: true, gate: 'post-done-self-check', reason: e.message };
|
|
1129
1276
|
}
|
|
1277
|
+
// v1.5.1 cleanup C1 — T20 truncation classification. When the caller hands
|
|
1278
|
+
// us the subagent's `events` stream and/or intent `journal` on the payload,
|
|
1279
|
+
// run `detectTruncation` so a truncated post-DONE is classified on the LIVE
|
|
1280
|
+
// path (ijfw_state MCP tool → query → subagent.post-done). This is the
|
|
1281
|
+
// production caller recovery/truncation.js was missing — its only prior
|
|
1282
|
+
// importer was the unwired runtime-loop.js. Annotation-only: the classifier
|
|
1283
|
+
// never throws and never alters the self-check verdict.
|
|
1284
|
+
let truncation;
|
|
1285
|
+
if (Array.isArray(payload?.events) || Array.isArray(payload?.journal)) {
|
|
1286
|
+
try {
|
|
1287
|
+
const { detectTruncation } = await import('../recovery/truncation.js');
|
|
1288
|
+
const det = detectTruncation({
|
|
1289
|
+
events: payload.events,
|
|
1290
|
+
journal: payload.journal,
|
|
1291
|
+
expectedTerminalVerb: payload.expectedTerminalVerb,
|
|
1292
|
+
});
|
|
1293
|
+
truncation = {
|
|
1294
|
+
truncated: det.truncated,
|
|
1295
|
+
reason: det.reason,
|
|
1296
|
+
};
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
process.stderr.write(
|
|
1299
|
+
`[state-sdk] WARN subagent.post-done truncation classify failed: ${e.message}\n`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1130
1303
|
if (selfCheck.verdict !== 'PASSED') {
|
|
1131
1304
|
const reason = `self-check FAILED — ${selfCheck.files_missing.length} missing file(s), `
|
|
1132
1305
|
+ `${selfCheck.commits_missing.length} missing commit(s)`;
|
|
1306
|
+
// v1.5.1: LIVE wiring of debug-trident (T29) onto the production
|
|
1307
|
+
// gate-failure path. When the post-done self-check FAILS this is
|
|
1308
|
+
// exactly the stalled-investigation moment the Trident debug loop
|
|
1309
|
+
// exists for — dispatch codex+gemini to generate competing root-cause
|
|
1310
|
+
// hypotheses against the gate-failure evidence. FIRE-AND-FORGET:
|
|
1311
|
+
// `maybeFireDebugTrident` returns immediately, the campaign runs in a
|
|
1312
|
+
// detached promise — the verb's return value + timing are UNCHANGED so
|
|
1313
|
+
// STATE-SDK-CONTRACT §8 (subagent.post-done is a fast read verb) holds.
|
|
1314
|
+
// Env-gated (IJFW_DEBUG_TRIDENT) + silent no-op on missing deps; never
|
|
1315
|
+
// throws. Dynamic import avoids a static require cycle. Mirrors the
|
|
1316
|
+
// A-Mem auto-linker fire-and-forget pattern in memory/fts5.js.
|
|
1317
|
+
try {
|
|
1318
|
+
import('./debug-trident-trigger.js')
|
|
1319
|
+
.then(({ maybeFireDebugTrident }) => {
|
|
1320
|
+
maybeFireDebugTrident({
|
|
1321
|
+
projectRoot,
|
|
1322
|
+
subagentId: _subagentId,
|
|
1323
|
+
reason,
|
|
1324
|
+
reportText,
|
|
1325
|
+
selfCheck,
|
|
1326
|
+
});
|
|
1327
|
+
})
|
|
1328
|
+
.catch((e) => {
|
|
1329
|
+
try {
|
|
1330
|
+
process.stderr.write(
|
|
1331
|
+
`[state-sdk] WARN subagent.post-done debug-trident dispatch failed: ${e.message}\n`,
|
|
1332
|
+
);
|
|
1333
|
+
} catch { /* never throw */ }
|
|
1334
|
+
});
|
|
1335
|
+
} catch { /* fire-and-forget — never alters the verb verdict */ }
|
|
1133
1336
|
if (!GATE_BYPASS) {
|
|
1134
1337
|
return {
|
|
1135
1338
|
ok: false, refused: true, gate: 'post-done-self-check', reason,
|
|
1339
|
+
...(truncation ? { truncation } : {}),
|
|
1136
1340
|
};
|
|
1137
1341
|
}
|
|
1138
1342
|
// Bypass masks a would-be refusal — emit a loud WARN + advisory
|
|
@@ -1149,6 +1353,7 @@ const handlers = {
|
|
|
1149
1353
|
claimedCommits: selfCheck.commits_claimed,
|
|
1150
1354
|
verified: false,
|
|
1151
1355
|
},
|
|
1356
|
+
...(truncation ? { truncation } : {}),
|
|
1152
1357
|
};
|
|
1153
1358
|
}
|
|
1154
1359
|
return {
|
|
@@ -1158,6 +1363,7 @@ const handlers = {
|
|
|
1158
1363
|
claimedCommits: selfCheck.commits_claimed,
|
|
1159
1364
|
verified: selfCheck.verdict === 'PASSED',
|
|
1160
1365
|
},
|
|
1366
|
+
...(truncation ? { truncation } : {}),
|
|
1161
1367
|
};
|
|
1162
1368
|
},
|
|
1163
1369
|
|
|
@@ -27,6 +27,10 @@ import { query } from './state-sdk.js';
|
|
|
27
27
|
import { pollEvents } from './state-events.js';
|
|
28
28
|
// v1.5.0 N4.obs M1+M2: trace-id + hierarchical observation path.
|
|
29
29
|
import { getTraceId, composePath } from '../observability/trace-id.js';
|
|
30
|
+
// v1.5.1 W2.F: checkpoint-contract evaluator (N4.obs M3). Validates a final
|
|
31
|
+
// status report carried by a checkpoint envelope against the v1.4.4 N2
|
|
32
|
+
// four-section contract before the envelope is written.
|
|
33
|
+
import { evaluateCheckpointContract } from '../observability/evaluator-checkpoint-contract.js';
|
|
30
34
|
|
|
31
35
|
// ---------------------------------------------------------------------------
|
|
32
36
|
// Frozen constants — Wave 11-A imports these directly
|
|
@@ -123,6 +127,21 @@ export async function recordCheckpoint(waveId, subId, checkpoint, projectRoot) {
|
|
|
123
127
|
...checkpoint,
|
|
124
128
|
};
|
|
125
129
|
|
|
130
|
+
// v1.5.1 W2.F: contract-validate at emission time. When a checkpoint
|
|
131
|
+
// envelope carries the subagent's final status report (`checkpoint.report`),
|
|
132
|
+
// it MUST satisfy the v1.4.4 N2 four-section contract (Status/SHAs/Files/
|
|
133
|
+
// Tests). Schema drift now fails LOUD here at write — not silently at read.
|
|
134
|
+
// Progress-only checkpoints (no `report` field) skip this check unchanged.
|
|
135
|
+
if (typeof checkpoint.report === 'string') {
|
|
136
|
+
const evalResult = evaluateCheckpointContract(checkpoint.report);
|
|
137
|
+
if (!evalResult.valid) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`subagent-telemetry: checkpoint report for ${waveId}/${subId} `
|
|
140
|
+
+ `violates checkpoint-contract — ${evalResult.reason}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
126
145
|
const serialised = JSON.stringify(envelope);
|
|
127
146
|
if (serialised.length > MAX_CHECKPOINT_SIZE) {
|
|
128
147
|
throw new Error(
|
|
@@ -59,6 +59,27 @@ function loadPopulateBlackboardBlock() {
|
|
|
59
59
|
return _populateBlackboardBlockPromise;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Wave 5B wiring (post-cross-audit W1 fix): same lazy-Promise-singleton
|
|
63
|
+
// pattern as populateBlackboardBlock above. populateDisciplineBlock is
|
|
64
|
+
// idempotent (no-op short-circuit when content unchanged), so firing on
|
|
65
|
+
// every wave checkpoint is free and guarantees the DISCIPLINE marker block
|
|
66
|
+
// in AGENTS.md actually gets populated during a real workflow — closes the
|
|
67
|
+
// "ships as dead code" wiring gap the cross-audit caught.
|
|
68
|
+
let _populateDisciplineBlockPromise = null;
|
|
69
|
+
function loadPopulateDisciplineBlock() {
|
|
70
|
+
if (_populateDisciplineBlockPromise === null) {
|
|
71
|
+
_populateDisciplineBlockPromise = (async () => {
|
|
72
|
+
try {
|
|
73
|
+
const mod = await import('./agents-md-blackboard.js');
|
|
74
|
+
return mod.populateDisciplineBlock ?? null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
}
|
|
80
|
+
return _populateDisciplineBlockPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
/**
|
|
63
84
|
* Test-only helper: reset the populateBlackboardBlock promise singleton so a
|
|
64
85
|
* test can simulate "first call after process start" semantics. Internal.
|
|
@@ -69,6 +90,14 @@ export function _resetPopulateBlackboardBlockSingleton() {
|
|
|
69
90
|
_populateBlackboardBlockPromise = null;
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Test-only helper: reset the populateDisciplineBlock promise singleton.
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export function _resetPopulateDisciplineBlockSingleton() {
|
|
98
|
+
_populateDisciplineBlockPromise = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
// ---------------------------------------------------------------------------
|
|
73
102
|
// Internal YAML helpers — flat subset only (string/number/boolean/string[])
|
|
74
103
|
// ---------------------------------------------------------------------------
|
|
@@ -560,5 +589,14 @@ export async function checkpointWave(waveId, projectRoot) {
|
|
|
560
589
|
try { await populateBlackboardBlock(waveId, projectRoot); } catch { /* advisory */ }
|
|
561
590
|
}
|
|
562
591
|
|
|
592
|
+
// Wave 5B wiring (cross-audit W1 fix): populate the DISCIPLINE block too.
|
|
593
|
+
// Same advisory-failure semantics as the BLACKBOARD call above. Auto-detects
|
|
594
|
+
// project type from .ijfw/memory/brief.md frontmatter or repo signals — no
|
|
595
|
+
// explicit projectType passed, the detector handles it.
|
|
596
|
+
const populateDisciplineBlock = await loadPopulateDisciplineBlock();
|
|
597
|
+
if (populateDisciplineBlock) {
|
|
598
|
+
try { await populateDisciplineBlock(projectRoot, undefined, { waveId }); } catch { /* advisory */ }
|
|
599
|
+
}
|
|
600
|
+
|
|
563
601
|
return next;
|
|
564
602
|
}
|
package/src/override-resolver.js
CHANGED
|
@@ -65,9 +65,11 @@ import {
|
|
|
65
65
|
* projectRoot. Used by deployResolvedSkill to know which platforms to write
|
|
66
66
|
* the merged body into.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
* installer/src/install-helpers.js once that module exposes a
|
|
70
|
-
* platform-list getter. Until then this on-disk probe is the
|
|
68
|
+
* DEFERRED (platform-list consolidation): could be replaced with an exported
|
|
69
|
+
* helper from installer/src/install-helpers.js once that module exposes a
|
|
70
|
+
* canonical platform-list getter. Until then this on-disk probe is the
|
|
71
|
+
* contract — and as an on-disk probe it correctly reflects what is actually
|
|
72
|
+
* deployed, so the consolidation is a nice-to-have, not a defect.
|
|
71
73
|
*
|
|
72
74
|
* @param {string} projectRoot
|
|
73
75
|
* @returns {string[]} absolute paths to existing platform skill dirs
|