@try-works/dsh-recursive-mode 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -8
- package/lib/enforcement.d.ts +58 -0
- package/lib/index.js +597 -107
- package/lib/memory.d.ts +35 -2
- package/lib/phase-rules.d.ts +102 -0
- package/lib/policy-globs.d.ts +5 -0
- package/lib/training.d.ts +240 -2
- package/package.json +1 -1
- package/references/artifact-template.md +26 -61
- package/references/bodies/claude.md +1 -1
- package/references/bodies/copilot.md +1 -1
- package/references/bodies/cursorrules.md +4 -2
- package/references/bodies/memory-router.md +1 -1
- package/references/bodies/recursive-agents-router.md +4 -3
- package/references/bootstrap/RECURSIVE.md +21 -30
- package/src/bootstrap.ts +30 -14
- package/src/enforcement.ts +89 -5
- package/src/index.ts +795 -723
- package/src/memory.ts +50 -9
- package/src/phase-rules.ts +162 -1
- package/src/policy-globs.ts +28 -4
- package/src/runtime.ts +1962 -1945
- package/src/training.ts +634 -6
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
4
4
|
import { Service } from "@deepseek-ai/cordis";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -1483,15 +1483,34 @@ const LOCK_TOOL_NAMES = /* @__PURE__ */ new Set(["recursive_lock", "recursive_lo
|
|
|
1483
1483
|
* sentence; the predicate adds the blocking artifact and its status, which is
|
|
1484
1484
|
* what distinguishes a guard refusal from `lockArtifact`'s own
|
|
1485
1485
|
* `Prerequisite blockers:` error (`tests/guard-path.spec.ts` asserts both).
|
|
1486
|
+
*
|
|
1487
|
+
* ⚠ ISSUE 2 (b) — AND IT NAMES THE RUN IT READ, `ctx.runId`, as `[run: <id>]`.
|
|
1488
|
+
*
|
|
1489
|
+
* The blockers above are read FROM A DIRECTORY (`runDir`), and until this suffix existed the refusal said
|
|
1490
|
+
* only "an earlier phase must be locked first 00-requirements.md (DRAFT)" — a sentence with no run in it.
|
|
1491
|
+
* That is what let a refusal MIX TWO RUNS in one payload: a call naming run-b could be judged against
|
|
1492
|
+
* run-a's tree (the guard resolved the run from the filesystem, the tool from `args.runId`) and the caller
|
|
1493
|
+
* was told about `00-requirements.md (DRAFT)` while the guard-decision record said `runId: run-a` and the
|
|
1494
|
+
* gate-block ask said `artifact: 01-as-is.md`. The blocking artifact, the artifact the caller named and the
|
|
1495
|
+
* run the record attributed it to were three answers to one question.
|
|
1496
|
+
*
|
|
1497
|
+
* The fix is two-sided: the guard now judges the run the CALL NAMES (see `resolveGuardRunId` in
|
|
1498
|
+
* `enforcement.ts`), and this suffix makes the evaluated run part of the sentence, so the payload can be
|
|
1499
|
+
* read without cross-referencing the log record — and a reader can SEE which run was read, which is what
|
|
1500
|
+
* makes a future mismatch visible instead of silent.
|
|
1501
|
+
*
|
|
1502
|
+
* `runId` is optional because the pure policy layer may be called with no run context at all (a policy
|
|
1503
|
+
* unit test, a preview probe): the sentence is then exactly what it always was, and no run is invented.
|
|
1486
1504
|
*/
|
|
1487
|
-
function lockOrderRule(artifact, runDir) {
|
|
1505
|
+
function lockOrderRule(artifact, runDir, runId) {
|
|
1488
1506
|
const name = String(artifact ?? "");
|
|
1489
1507
|
if (!name || !runDir) return null;
|
|
1490
1508
|
const blockers = getPrerequisiteBlockers(runDir, name);
|
|
1491
1509
|
if (blockers.length === 0) return null;
|
|
1510
|
+
const where = typeof runId === "string" && runId.trim() !== "" ? " [run: " + runId.trim() + "]" : "";
|
|
1492
1511
|
return {
|
|
1493
1512
|
verdict: "deny",
|
|
1494
|
-
detail: blockers.map((b) => b.artifact + " (" + b.status + ")").join(", "),
|
|
1513
|
+
detail: blockers.map((b) => b.artifact + " (" + b.status + ")").join(", ") + where,
|
|
1495
1514
|
blockers
|
|
1496
1515
|
};
|
|
1497
1516
|
}
|
|
@@ -1622,7 +1641,7 @@ function builtInToolPolicyRules() {
|
|
|
1622
1641
|
verdict: "deny",
|
|
1623
1642
|
reason: "monotonic lock-order: an earlier phase must be locked first",
|
|
1624
1643
|
label: "lock-order",
|
|
1625
|
-
predicate: (id, args, ctx) => LOCK_TOOL_NAMES.has(id) ? lockOrderRule(args.artifact, ctx.runDir) : null
|
|
1644
|
+
predicate: (id, args, ctx) => LOCK_TOOL_NAMES.has(id) ? lockOrderRule(args.artifact, ctx.runDir, ctx.runId) : null
|
|
1626
1645
|
}];
|
|
1627
1646
|
for (const name of WRITE_TOOL_NAMES) rules.push({
|
|
1628
1647
|
pattern: name,
|
|
@@ -1674,7 +1693,7 @@ function attachPolicyPredicate(rule) {
|
|
|
1674
1693
|
if (rule.predicate) return rule;
|
|
1675
1694
|
if (rule.pattern === "recursive_lock*") return {
|
|
1676
1695
|
...rule,
|
|
1677
|
-
predicate: (id, args, ctx) => LOCK_TOOL_NAMES.has(id) ? lockOrderRule(args.artifact, ctx.runDir) : null
|
|
1696
|
+
predicate: (id, args, ctx) => LOCK_TOOL_NAMES.has(id) ? lockOrderRule(args.artifact, ctx.runDir, ctx.runId) : null
|
|
1678
1697
|
};
|
|
1679
1698
|
if (rule.label === "phase-order") return {
|
|
1680
1699
|
...rule,
|
|
@@ -2028,6 +2047,87 @@ const SECTION_MAP = {
|
|
|
2028
2047
|
]
|
|
2029
2048
|
};
|
|
2030
2049
|
/**
|
|
2050
|
+
* T40 — `.recursive/memory/`, the plane THIS plugin writes, and the phase-8 step that was prose.
|
|
2051
|
+
*
|
|
2052
|
+
* ⚠ WHY THIS EXISTS, MEASURED. Three completed runs in a live workspace left `.recursive/memory/`
|
|
2053
|
+
* exactly as `bootstrap.ts` scaffolded it: `MEMORY.md` and the skill docs were still the
|
|
2054
|
+
* bootstrap-created placeholders, while run 03's `08-memory-impact.md` had its
|
|
2055
|
+
* "Write the durable ones to memory, with provenance" box TICKED and its `Inputs` line naming
|
|
2056
|
+
* ANOTHER plugin's store (`memory_search` / `memory_status` is dsh-memory, not this plugin). So the
|
|
2057
|
+
* phase declared its memory step done against a system this plugin does not own, and the plugin's
|
|
2058
|
+
* own learning never activated — the owner's words: *"the agent should write to .recursive/memory/
|
|
2059
|
+
* in phase 8, that's a hard requirement that needs to be enforced"*.
|
|
2060
|
+
*
|
|
2061
|
+
* ⚠ THE FIX IS A FACT, NOT A STRONGER SENTENCE. A prose step is ticked by the agent that would have
|
|
2062
|
+
* had to do it, which is exactly what happened. What follows is the same requirement in the form the
|
|
2063
|
+
* workflow can CHECK: a path under the plane, declared in the artifact, whose own text on disk
|
|
2064
|
+
* carries this run's provenance (`Source-Runs`). "The run wrote its durable memory" then stops being
|
|
2065
|
+
* a claim about the run's intentions and becomes a claim about files.
|
|
2066
|
+
*
|
|
2067
|
+
* ⚠ WHY IT IS NOT IN `SECTION_MAP`, deliberately: that map is byte-parity with the canonical
|
|
2068
|
+
* linter's `get_artifact_required_sections` (`tests/phase-rules.parity.spec.ts` pins all twelve
|
|
2069
|
+
* lists), and a section added there would be a parity break dressed up as a feature. The requirement
|
|
2070
|
+
* rides on a section the canonical template ALREADY scaffolds — `## Affected Memory Docs` — plus a
|
|
2071
|
+
* field the plane's own linter already requires, so the artifact shape stays canonical.
|
|
2072
|
+
*/
|
|
2073
|
+
const PHASE8_MEMORY_ARTIFACT = "08-memory-impact.md";
|
|
2074
|
+
/** The plane, repo-relative. The trailing slash is what every guard here tests for. */
|
|
2075
|
+
const MEMORY_PLANE_PREFIX = ".recursive/memory/";
|
|
2076
|
+
/** The canonical section the declaration goes in — present in every scaffolded phase-8 artifact. */
|
|
2077
|
+
const PHASE8_MEMORY_SECTION = "Affected Memory Docs";
|
|
2078
|
+
/**
|
|
2079
|
+
* Where a durable doc may be filed, and the `Type` the memory-plane linter requires for it.
|
|
2080
|
+
*
|
|
2081
|
+
* ⚠ EVERY `dir` IS THE REAL PLANE, `.recursive/memory/…`, AND NOT A `memory/…` RELATIVE FORM. Measured:
|
|
2082
|
+
* `bootstrap.ts` scaffolds the plane under `.recursive/`, `ts-lint.ts` lints it there, and the phase-8
|
|
2083
|
+
* artifact cites it there — while the training trigger's own write seam joins its `memory/…` paths onto
|
|
2084
|
+
* the workspace root, i.e. `<root>/memory/`, a directory that exists in no real workspace (the reader
|
|
2085
|
+
* side of that defect was fixed in `memory.ts`; the writer side is `runtime.ts`'s seam and is reported,
|
|
2086
|
+
* not edited). This table names the location a WRITE must land in, so it names the plane.
|
|
2087
|
+
*
|
|
2088
|
+
* ⚠ `skill` SHARES `pattern`'s Type ON PURPOSE: `/.recursive/memory/skills/patterns/` is where a
|
|
2089
|
+
* promoted skill lesson ships (`bootstrap.ts` scaffolds three docs there), and the linter's allowed
|
|
2090
|
+
* Types are `index|domain|pattern|incident|episode` — there is no `skill` Type to declare.
|
|
2091
|
+
*/
|
|
2092
|
+
const MEMORY_DOC_LOCATIONS = {
|
|
2093
|
+
domain: {
|
|
2094
|
+
dir: ".recursive/memory/domains",
|
|
2095
|
+
type: "domain"
|
|
2096
|
+
},
|
|
2097
|
+
pattern: {
|
|
2098
|
+
dir: ".recursive/memory/patterns",
|
|
2099
|
+
type: "pattern"
|
|
2100
|
+
},
|
|
2101
|
+
incident: {
|
|
2102
|
+
dir: ".recursive/memory/incidents",
|
|
2103
|
+
type: "incident"
|
|
2104
|
+
},
|
|
2105
|
+
episode: {
|
|
2106
|
+
dir: ".recursive/memory/episodes",
|
|
2107
|
+
type: "episode"
|
|
2108
|
+
},
|
|
2109
|
+
skill: {
|
|
2110
|
+
dir: ".recursive/memory/skills/patterns",
|
|
2111
|
+
type: "pattern"
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
/** The field that makes a doc THIS run's. It is the phase-8 gate's entire discriminator. */
|
|
2115
|
+
const MEMORY_PROVENANCE_FIELD = "Source-Runs";
|
|
2116
|
+
const PHASE8_MEMORY_WRITE_RULE = {
|
|
2117
|
+
artifact: PHASE8_MEMORY_ARTIFACT,
|
|
2118
|
+
plane: MEMORY_PLANE_PREFIX,
|
|
2119
|
+
section: PHASE8_MEMORY_SECTION,
|
|
2120
|
+
provenanceField: MEMORY_PROVENANCE_FIELD,
|
|
2121
|
+
alwaysAvailable: ".recursive/memory/episodes/<run-id>.md",
|
|
2122
|
+
kinds: Object.keys(MEMORY_DOC_LOCATIONS),
|
|
2123
|
+
summary: "HARD: this run must have WRITTEN at least one doc under .recursive/memory/ before 08-memory-impact.md locks — .recursive/memory/episodes/<run-id>.md is always available — declared by path under `## Affected Memory Docs` and carrying `Source-Runs: <this-run-id>`; citing a shard this run did not write does not count.",
|
|
2124
|
+
instruction: "HARD REQUIREMENT, CHECKED AT LOCK: before 08-memory-impact.md locks, this run must have WRITTEN at least one doc under .recursive/memory/ and declared that path under `## Affected Memory Docs`. A declared path counts ONLY when the doc on disk carries `Source-Runs` naming THIS run, because that is what separates \"the run wrote its memory\" from \"the run cited someone else's\". Render the doc with the metadata the memory-plane lint requires (Type, Status, Scope, Owns-Paths, Watch-Paths, Source-Runs, Validated-At-Commit, Last-Validated, Tags): .recursive/memory/episodes/<run-id>.md is always available for a run-local lesson, `.recursive/memory/domains/`, `.recursive/memory/patterns/` and `.recursive/memory/incidents/` hold generalized knowledge, and `.recursive/memory/skills/patterns/` is where a promoted skill lesson belongs. A doc missing a required field, or carrying a Type/Status the plane lint rejects, FAILS the memory plane — write it in the canonical shape. The written path enters the run diff under .recursive/memory/, which phase 8 OWNS in its Worktree Diff Audit and Requirement Completion Status."
|
|
2125
|
+
};
|
|
2126
|
+
/** The memory-write rule for an artifact, or null when that phase owes no memory write. */
|
|
2127
|
+
function phase8MemoryWriteRuleFor(fileName) {
|
|
2128
|
+
return fileName === "08-memory-impact.md" ? PHASE8_MEMORY_WRITE_RULE : null;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2031
2131
|
* get_artifact_required_sections(file_name, workflow_profile): canonical-parity
|
|
2032
2132
|
* required section headings for a phase artifact. Defaults to TODO + Coverage
|
|
2033
2133
|
* Gate + Approval Gate for unknown files. Audited phases in strict profiles get
|
|
@@ -2069,7 +2169,8 @@ function phaseRulesFor(fileName, workflowProfile = CURRENT_WORKFLOW_PROFILE) {
|
|
|
2069
2169
|
requiredSections: getArtifactRequiredSections(fileName, workflowProfile),
|
|
2070
2170
|
audited: AUDITED_PHASE_FILES$1.has(fileName),
|
|
2071
2171
|
tdd: fileName === "03-implementation-summary.md",
|
|
2072
|
-
qa: fileName === "05-manual-qa.md"
|
|
2172
|
+
qa: fileName === "05-manual-qa.md",
|
|
2173
|
+
memoryWrite: phase8MemoryWriteRuleFor(fileName)
|
|
2073
2174
|
};
|
|
2074
2175
|
}
|
|
2075
2176
|
/**
|
|
@@ -2088,6 +2189,7 @@ function phaseLintRulesMessage(fileName, workflowProfile = CURRENT_WORKFLOW_PROF
|
|
|
2088
2189
|
"Audited phases: end with Audit: PASS before setting Coverage/Approval PASS; record Audit Context and Audit Verdict.",
|
|
2089
2190
|
"TDD (phase 3): declare TDD Mode: strict|pragmatic; strict requires RED + GREEN evidence paths.",
|
|
2090
2191
|
"QA (phase 5): declare QA Execution Mode: human|agent-operated|hybrid; human/hybrid need user sign-off.",
|
|
2192
|
+
...rules.memoryWrite === null ? [] : ["Memory write (phase 8, HARD): " + rules.memoryWrite.summary],
|
|
2091
2193
|
"</system-reminder>"
|
|
2092
2194
|
].join("\n");
|
|
2093
2195
|
}
|
|
@@ -2184,7 +2286,7 @@ function phaseBaselineRules(fileName) {
|
|
|
2184
2286
|
pattern: "write*",
|
|
2185
2287
|
verdict: "deny",
|
|
2186
2288
|
reason: "phase " + phase + " is a documentation phase: writes outside the run tree are denied (the implementation is frozen)",
|
|
2187
|
-
predicate: (_id, args, ctx) => writesOutsideRunTree(args, ctx) ? { verdict: "deny" } : null
|
|
2289
|
+
predicate: (_id, args, ctx) => writesOutsideRunTree(args, ctx) && !(phase === "8" && writesOwnMemoryPlane(args, ctx)) ? { verdict: "deny" } : null
|
|
2188
2290
|
});
|
|
2189
2291
|
if (phase === "6" || phase === "7") rules.push({
|
|
2190
2292
|
pattern: "write*",
|
|
@@ -2222,6 +2324,23 @@ function memoryPlanePath(abs) {
|
|
|
2222
2324
|
return /\/(decisions|state)\.md$/i.test(normalized) || /\/\.recursive\/memory(\/|$)/.test(normalized);
|
|
2223
2325
|
}
|
|
2224
2326
|
/**
|
|
2327
|
+
* True when the call writes `.recursive/memory/**` — THIS PLUGIN'S OWN plane, and nothing else.
|
|
2328
|
+
*
|
|
2329
|
+
* ⚠ DELIBERATELY NARROWER THAN {@link writesMemoryPlane}, which also matches `DECISIONS.md` and
|
|
2330
|
+
* `STATE.md`: phase 8's carve-out (T40) is about durable memory, not about handing phase 8 the two
|
|
2331
|
+
* planes phases 6-7 own.
|
|
2332
|
+
*
|
|
2333
|
+
* ⚠ AND AN UNRESOLVABLE TARGET IS `false` HERE — the OPPOSITE of every other fail-closed answer in
|
|
2334
|
+
* this file, because this is the PERMISSIVE branch: `true` means "do not deny", so a path the rules
|
|
2335
|
+
* cannot place must not be admitted by it. "We could not tell where this lands" is a reason to
|
|
2336
|
+
* refuse, never a reason to allow.
|
|
2337
|
+
*/
|
|
2338
|
+
function writesOwnMemoryPlane(args, ctx) {
|
|
2339
|
+
const abs = baselineTarget(args, ctx);
|
|
2340
|
+
if (abs === "unresolvable") return false;
|
|
2341
|
+
return /\/\.recursive\/memory(\/|$)/.test(abs.replace(/\\/g, "/"));
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2225
2344
|
* Resolve a tool-target path to an absolute path. Mirrors enforcement.ts's
|
|
2226
2345
|
* resolution rules: an absolute path stays as it is; a relative path resolves
|
|
2227
2346
|
* against the worktree root. `null` when the value cannot be a path at all.
|
|
@@ -6326,10 +6445,23 @@ function defaultWrite(path, content) {
|
|
|
6326
6445
|
* so the result is reproducible from the inputs alone.
|
|
6327
6446
|
*/
|
|
6328
6447
|
/** The kinds this plugin's memory layer holds, in the order the scaffold creates them. */
|
|
6448
|
+
/**
|
|
6449
|
+
* ⚠ `training` IS IN THIS LIST BECAUSE IT IS THE KIND THE PLUGIN'S OWN TRAINING PATH WRITES.
|
|
6450
|
+
*
|
|
6451
|
+
* The phase-8 trigger (`training.ts`) writes `memory/training/<task-type>.md` and advertises that path in
|
|
6452
|
+
* `memory/MEMORY.md`; the shipped router (`references/bodies/memory-router.md`) tells an agent to load "the
|
|
6453
|
+
* relevant docs under `/.recursive/memory/training/`". With `training` absent from this list the loader
|
|
6454
|
+
* COULD NOT SEE THE SHARDS THE TRIGGER WROTE — the writer's own output was unreachable by the reader, so
|
|
6455
|
+
* "the plane's next run scores it" (README §10) was not true of exactly the shards training produces.
|
|
6456
|
+
*
|
|
6457
|
+
* `incidents/` and `archive/` stay out deliberately: `archive/` is historical by the router's own definition,
|
|
6458
|
+
* and widening retrieval to `incidents/` is a separate ranking decision that this change does not make.
|
|
6459
|
+
*/
|
|
6329
6460
|
const MEMORY_KINDS = [
|
|
6330
6461
|
"domains",
|
|
6331
6462
|
"patterns",
|
|
6332
6463
|
"episodes",
|
|
6464
|
+
"training",
|
|
6333
6465
|
"skills"
|
|
6334
6466
|
];
|
|
6335
6467
|
/**
|
|
@@ -6454,6 +6586,27 @@ function entryAppliesTo(entry) {
|
|
|
6454
6586
|
if (!match || match[1] === void 0) return [];
|
|
6455
6587
|
return match[1].split(",").map((part) => part.replace(/[^0-9.]/g, "")).filter((part) => part !== "");
|
|
6456
6588
|
}
|
|
6589
|
+
/**
|
|
6590
|
+
* Where the plane may live under a workspace root, in PREFERENCE order.
|
|
6591
|
+
*
|
|
6592
|
+
* ⚠ THE MEASURED DEFECT THIS FIXES. `defaultMemoryList` used to join `memory/<kind>/` straight onto the
|
|
6593
|
+
* root it was handed — i.e. `<root>/memory/` — and that directory EXISTS IN NO REAL WORKSPACE. `bootstrap.ts`
|
|
6594
|
+
* scaffolds the plane at `<root>/.recursive/memory/`, `ts-lint.ts` lints it there, and the review bundle reads
|
|
6595
|
+
* it there; this loader alone looked beside it. Measured live on a workspace whose `.recursive/memory/` was
|
|
6596
|
+
* scaffolded and whose `<root>/memory/` did not exist: `selectMemory` reported "the memory plane is empty"
|
|
6597
|
+
* over a plane that was there, every phase of every run — and the training shards `training.ts` writes were
|
|
6598
|
+
* therefore unreachable by the loader that is supposed to score them.
|
|
6599
|
+
*
|
|
6600
|
+
* ⚠ PREFER, THEN FALL BACK — NEVER MERGE. This is the rule `readFeedback` already follows for its own moved
|
|
6601
|
+
* sidecar (`memory-feedback.ts`: `FEEDBACK_FILE` then `LEGACY_FEEDBACK_FILE`): the current location wins, and
|
|
6602
|
+
* the earlier one is consulted ONLY when the current one yields nothing, because two snapshots of one shard
|
|
6603
|
+
* added together would count a shard twice and a duplicated shard would outrank a real one.
|
|
6604
|
+
*
|
|
6605
|
+
* ⚠ AND IT MAKES THE CALLER'S CONVENTION IRRELEVANT. Passing the workspace root resolves
|
|
6606
|
+
* `<root>/.recursive/memory/`; passing `.recursive` itself resolves through the second entry. Both are
|
|
6607
|
+
* accepted, which is what README §6 means by "using `.recursive` as the root is selected again".
|
|
6608
|
+
*/
|
|
6609
|
+
const MEMORY_PLANE_BASES = [".recursive/memory", "memory"];
|
|
6457
6610
|
/** Read the whole plane, minus nothing: filtering is the SELECTOR's job, not the reader's. */
|
|
6458
6611
|
function loadMemoryIndex(root, readFile = defaultMemoryRead, listFiles = (kind) => defaultMemoryList(root, kind)) {
|
|
6459
6612
|
return readMemoryEntries(readFile, listFiles);
|
|
@@ -6517,12 +6670,14 @@ function defaultMemoryRead(path) {
|
|
|
6517
6670
|
}
|
|
6518
6671
|
}
|
|
6519
6672
|
function defaultMemoryList(root, kind) {
|
|
6520
|
-
const
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6673
|
+
for (const base of MEMORY_PLANE_BASES) {
|
|
6674
|
+
const dir = join(root, base, kind);
|
|
6675
|
+
try {
|
|
6676
|
+
const files = readdirSync(dir).filter((name) => name.endsWith(".md")).map((name) => join(dir, name));
|
|
6677
|
+
if (files.length > 0) return files;
|
|
6678
|
+
} catch {}
|
|
6525
6679
|
}
|
|
6680
|
+
return [];
|
|
6526
6681
|
}
|
|
6527
6682
|
//#endregion
|
|
6528
6683
|
//#region src/training.ts
|
|
@@ -6551,7 +6706,7 @@ function defaultMemoryList(root, kind) {
|
|
|
6551
6706
|
* history of a learning stays readable; and a PINNED entry is untouchable by every automatic path.
|
|
6552
6707
|
*/
|
|
6553
6708
|
/** The artifact whose lock marks a run as complete enough to learn from. */
|
|
6554
|
-
const PHASE8_ARTIFACT =
|
|
6709
|
+
const PHASE8_ARTIFACT = PHASE8_MEMORY_ARTIFACT;
|
|
6555
6710
|
/** The parent's exit codes, kept as names so a caller cannot mistake one failure for the other. */
|
|
6556
6711
|
const TRAINING_EXIT = {
|
|
6557
6712
|
/** The extractor could not be reached or run. */
|
|
@@ -6742,13 +6897,30 @@ function runPhase8Trigger(root, runId, options = {}) {
|
|
|
6742
6897
|
*
|
|
6743
6898
|
* ⚠ ONE ITEM PER RUN IS NAMED, so a reader can trace a learning back to the run that produced it —
|
|
6744
6899
|
* and the group is never presented as more evidence than it is.
|
|
6900
|
+
*
|
|
6901
|
+
* ⚠ T40 — AND IT NOW CARRIES THE PLANE'S METADATA HEADER, which it did not before. `memory/domains/
|
|
6902
|
+
* <subsystem>.md` is a doc the memory-plane lint validates like any other, and this renderer wrote a
|
|
6903
|
+
* bare `# Learnings:` heading — so the plugin's own cross-run extraction produced a doc its own
|
|
6904
|
+
* `lint_memory_plane` FAILS for nine missing fields. The extraction was right and its output shape was
|
|
6905
|
+
* wrong, which is exactly the kind of defect a write surface exists to prevent.
|
|
6745
6906
|
*/
|
|
6746
|
-
function renderGroupShard(group) {
|
|
6907
|
+
function renderGroupShard(group, options = {}) {
|
|
6908
|
+
const runs = [...new Set(group.items.map((item) => item.runId))];
|
|
6747
6909
|
const lines = [
|
|
6910
|
+
...renderMemoryMetadata({
|
|
6911
|
+
type: "domain",
|
|
6912
|
+
status: "CURRENT",
|
|
6913
|
+
scope: "Learnings extracted for subsystem " + group.subsystem + " (" + group.mode + ") from " + runs.length + " run(s).",
|
|
6914
|
+
sourceRuns: runs,
|
|
6915
|
+
validatedAtCommit: "extracted-at-run-close",
|
|
6916
|
+
lastValidated: options.lastValidated ?? isoSeconds(),
|
|
6917
|
+
tags: [group.subsystem, group.mode]
|
|
6918
|
+
}).trimEnd().split("\n"),
|
|
6919
|
+
"",
|
|
6748
6920
|
"# Learnings: " + group.subsystem,
|
|
6749
6921
|
"",
|
|
6750
6922
|
"- Mode: " + group.mode,
|
|
6751
|
-
"- Runs: " + group.runs + " (" +
|
|
6923
|
+
"- Runs: " + group.runs + " (" + runs.join(", ") + ")",
|
|
6752
6924
|
""
|
|
6753
6925
|
];
|
|
6754
6926
|
for (const item of group.items) lines.push("- [" + item.runId + "] " + item.text);
|
|
@@ -6909,8 +7081,20 @@ function updateMemoryRegistry(existing, entries) {
|
|
|
6909
7081
|
function taskTypeShardPath(mode) {
|
|
6910
7082
|
return "memory/training/" + mode + ".md";
|
|
6911
7083
|
}
|
|
6912
|
-
|
|
7084
|
+
/** T40: same metadata-header reason as {@link renderGroupShard} — see the note there. */
|
|
7085
|
+
function renderTaskTypeShard(groups, options = {}) {
|
|
7086
|
+
const runs = [...new Set(groups.flatMap((group) => group.items.map((item) => item.runId)))];
|
|
6913
7087
|
const lines = [
|
|
7088
|
+
...renderMemoryMetadata({
|
|
7089
|
+
type: "pattern",
|
|
7090
|
+
status: "CURRENT",
|
|
7091
|
+
scope: "Training shards extracted under mode " + groups[0].mode + ", one section per subsystem group.",
|
|
7092
|
+
sourceRuns: runs,
|
|
7093
|
+
validatedAtCommit: "extracted-at-run-close",
|
|
7094
|
+
lastValidated: options.lastValidated ?? isoSeconds(),
|
|
7095
|
+
tags: ["training", groups[0].mode]
|
|
7096
|
+
}).trimEnd().split("\n"),
|
|
7097
|
+
"",
|
|
6914
7098
|
"# Training shards: " + groups[0].mode,
|
|
6915
7099
|
"",
|
|
6916
7100
|
"Groups extracted under this mode, one section each. Learning happens through files, not model mutation.",
|
|
@@ -6975,6 +7159,184 @@ function extractAndGroup(runner, env, options = {}) {
|
|
|
6975
7159
|
groups: groupLearnings(items, options.isWinner ?? (() => true))
|
|
6976
7160
|
};
|
|
6977
7161
|
}
|
|
7162
|
+
/** `2026-10-10T08:39:59Z` — the lock fields' own timestamp shape, and the docs' `Last-Validated` one. */
|
|
7163
|
+
function isoSeconds(now = /* @__PURE__ */ new Date()) {
|
|
7164
|
+
return now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7165
|
+
}
|
|
7166
|
+
function bullets(values) {
|
|
7167
|
+
return (values ?? []).map((value) => "- `" + value.replace(/`/g, "'") + "`");
|
|
7168
|
+
}
|
|
7169
|
+
/**
|
|
7170
|
+
* The metadata header every durable doc carries: the nine fields `lint_memory_doc` requires, in the
|
|
7171
|
+
* order the SHIPPED docs use them (`Owns-Paths:` / `Watch-Paths:` / `Tags:` stand bare when empty,
|
|
7172
|
+
* which is what the workspace's own promoted docs do and what `has_header_field` accepts).
|
|
7173
|
+
*
|
|
7174
|
+
* ⚠ A BACKTICK INSIDE A FIELD VALUE IS REPLACED, NOT ESCAPED, because these values are read back by
|
|
7175
|
+
* a line-based field reader: a stray backtick would end the value early and leave the rest of the
|
|
7176
|
+
* sentence in the doc as if it were a field.
|
|
7177
|
+
*/
|
|
7178
|
+
function renderMemoryMetadata(input) {
|
|
7179
|
+
const lines = [
|
|
7180
|
+
"Type: `" + input.type + "`",
|
|
7181
|
+
"Status: `" + input.status + "`",
|
|
7182
|
+
"Scope: `" + input.scope.replace(/`/g, "'") + "`",
|
|
7183
|
+
"Owns-Paths:",
|
|
7184
|
+
...bullets(input.ownsPaths),
|
|
7185
|
+
"Watch-Paths:",
|
|
7186
|
+
...bullets(input.watchPaths),
|
|
7187
|
+
"Source-Runs:",
|
|
7188
|
+
...bullets(input.sourceRuns),
|
|
7189
|
+
"Validated-At-Commit: `" + input.validatedAtCommit.replace(/`/g, "'") + "`",
|
|
7190
|
+
"Last-Validated: `" + input.lastValidated.replace(/`/g, "'") + "`",
|
|
7191
|
+
"Tags:",
|
|
7192
|
+
...bullets(input.tags)
|
|
7193
|
+
];
|
|
7194
|
+
if (input.parent !== void 0 && input.parent.trim() !== "") lines.push("Parent: `" + input.parent.replace(/`/g, "'") + "`");
|
|
7195
|
+
return lines.join("\n") + "\n";
|
|
7196
|
+
}
|
|
7197
|
+
function unquoteMemoryValue(value) {
|
|
7198
|
+
const trimmed = value.trim();
|
|
7199
|
+
for (const quote of [
|
|
7200
|
+
"`",
|
|
7201
|
+
"\"",
|
|
7202
|
+
"'"
|
|
7203
|
+
]) if (trimmed.length >= 2 && trimmed.startsWith(quote) && trimmed.endsWith(quote)) return trimmed.slice(1, -1).trim();
|
|
7204
|
+
return trimmed;
|
|
7205
|
+
}
|
|
7206
|
+
/**
|
|
7207
|
+
* The values of a list field (`Source-Runs:`), inline or as bullets.
|
|
7208
|
+
*
|
|
7209
|
+
* ⚠ THE BLOCK ENDS AT THE FIRST BLANK LINE, HEADING OR NEW FIELD, and that strictness is the point:
|
|
7210
|
+
* the alternative is a parser that reads an unrelated bullet list further down the document as
|
|
7211
|
+
* provenance — and provenance is the one thing here that must never be guessed.
|
|
7212
|
+
*/
|
|
7213
|
+
function parseMemoryListField(content, fieldName) {
|
|
7214
|
+
const fieldRe = new RegExp("^[ \\t]*(?:[-*][ \\t]+)?" + fieldName + ":[ \\t]*(.*)$");
|
|
7215
|
+
const values = [];
|
|
7216
|
+
let inField = false;
|
|
7217
|
+
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
7218
|
+
const field = fieldRe.exec(line);
|
|
7219
|
+
if (field !== null) {
|
|
7220
|
+
inField = true;
|
|
7221
|
+
const inline = unquoteMemoryValue(field[1]);
|
|
7222
|
+
if (inline !== "") values.push(inline);
|
|
7223
|
+
continue;
|
|
7224
|
+
}
|
|
7225
|
+
if (!inField) continue;
|
|
7226
|
+
const item = /^[ \t]*[-*][ \t]+(.+)$/.exec(line);
|
|
7227
|
+
if (item !== null) {
|
|
7228
|
+
const value = unquoteMemoryValue(item[1]);
|
|
7229
|
+
if (value !== "") values.push(value);
|
|
7230
|
+
continue;
|
|
7231
|
+
}
|
|
7232
|
+
break;
|
|
7233
|
+
}
|
|
7234
|
+
return values;
|
|
7235
|
+
}
|
|
7236
|
+
/** The runs a doc's `Source-Runs` names. EXACT matches: `run-1` is not `run-10`. */
|
|
7237
|
+
function memoryDocProvenance(content) {
|
|
7238
|
+
return parseMemoryListField(content, MEMORY_PROVENANCE_FIELD);
|
|
7239
|
+
}
|
|
7240
|
+
function readTextOrNull(path) {
|
|
7241
|
+
try {
|
|
7242
|
+
return readFileSync(path, "utf8");
|
|
7243
|
+
} catch {
|
|
7244
|
+
return null;
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
/** Every `.recursive/memory/**` path a text declares, in the absolute or repo-relative spelling. */
|
|
7248
|
+
function phase8MemoryRefs(text) {
|
|
7249
|
+
const found = /* @__PURE__ */ new Set();
|
|
7250
|
+
const absolute = /(?:^|[\s(`'"<[])\/?\.recursive\/memory\/[A-Za-z0-9._@\-/]*\.md/g;
|
|
7251
|
+
const relative = /(?:^|[\s(`'"<[])(memory\/(?:domains|patterns|incidents|episodes|training|skills|archive)\/[A-Za-z0-9._@\-/]*\.md)/g;
|
|
7252
|
+
let match;
|
|
7253
|
+
while ((match = absolute.exec(text)) !== null) found.add(match[0].replace(/^[\s(`'"<[]/, "").replace(/^\/+/, ""));
|
|
7254
|
+
while ((match = relative.exec(text)) !== null) found.add(MEMORY_PLANE_PREFIX + match[1].slice(7));
|
|
7255
|
+
return [...found].sort();
|
|
7256
|
+
}
|
|
7257
|
+
/**
|
|
7258
|
+
* T40 — THE CHECKABLE FACT BEHIND "the run wrote its durable memory".
|
|
7259
|
+
*
|
|
7260
|
+
* Three conditions, and each one exists because of a way the claim could be made without the work
|
|
7261
|
+
* being done: the artifact DECLARES a path under the plane (not prose about memory in general); the
|
|
7262
|
+
* path EXISTS (a declaration is not a write); and the doc on disk carries `Source-Runs` naming THIS
|
|
7263
|
+
* run (a shard the run merely read, or one an earlier run wrote, is not this run's memory).
|
|
7264
|
+
*
|
|
7265
|
+
* ⚠ WHAT IT CANNOT TELL, stated rather than hidden: WHEN the doc was written. A run that wrote a
|
|
7266
|
+
* memory doc before phase 8 and cites it here passes — and the phase 6/7 baselines deny memory-plane
|
|
7267
|
+
* writes outright (`phaseBaselineRules`), so within this workflow the only phase that can produce
|
|
7268
|
+
* such a doc is 8. The check is about the FACT existing at lock time, not about the clock.
|
|
7269
|
+
*/
|
|
7270
|
+
function phase8MemoryEvidence(root, runId, artifactText, readText = readTextOrNull) {
|
|
7271
|
+
const declared = phase8MemoryRefs(artifactText);
|
|
7272
|
+
if (declared.length === 0) return {
|
|
7273
|
+
ok: false,
|
|
7274
|
+
declared,
|
|
7275
|
+
existing: [],
|
|
7276
|
+
written: [],
|
|
7277
|
+
reason: "the artifact declares no path under .recursive/memory/ at all"
|
|
7278
|
+
};
|
|
7279
|
+
const existing = [];
|
|
7280
|
+
const written = [];
|
|
7281
|
+
for (const path of declared) {
|
|
7282
|
+
const text = readText(join(root, path));
|
|
7283
|
+
if (text === null) continue;
|
|
7284
|
+
existing.push(path);
|
|
7285
|
+
if (memoryDocProvenance(text).includes(runId)) written.push(path);
|
|
7286
|
+
}
|
|
7287
|
+
if (written.length > 0) return {
|
|
7288
|
+
ok: true,
|
|
7289
|
+
declared,
|
|
7290
|
+
existing,
|
|
7291
|
+
written,
|
|
7292
|
+
reason: written.length + " declared memory doc(s) carry Source-Runs naming " + runId + ": " + written.join(", ")
|
|
7293
|
+
};
|
|
7294
|
+
if (existing.length === 0) return {
|
|
7295
|
+
ok: false,
|
|
7296
|
+
declared,
|
|
7297
|
+
existing,
|
|
7298
|
+
written,
|
|
7299
|
+
reason: "the artifact declares " + declared.length + " path(s) under .recursive/memory/ (" + declared.join(", ") + "), but none of them exists, so no memory doc was written"
|
|
7300
|
+
};
|
|
7301
|
+
return {
|
|
7302
|
+
ok: false,
|
|
7303
|
+
declared,
|
|
7304
|
+
existing,
|
|
7305
|
+
written,
|
|
7306
|
+
reason: "the artifact declares " + existing.length + " existing memory doc(s) (" + existing.join(", ") + "), but none carries Source-Runs naming run " + runId + " — citing a shard this run did not write is not a write"
|
|
7307
|
+
};
|
|
7308
|
+
}
|
|
7309
|
+
/**
|
|
7310
|
+
* T40 — THE REFUSAL, as a sentence, or null when the run may lock.
|
|
7311
|
+
*
|
|
7312
|
+
* ⚠ THE MESSAGE NAMES THE REMEDY, not only the fault. A refusal that says "no memory doc" leaves the
|
|
7313
|
+
* agent to guess a format it cannot guess (nine fields, five allowed Types, a provenance list), which
|
|
7314
|
+
* is how the step became a ticked box in the first place.
|
|
7315
|
+
*/
|
|
7316
|
+
function phase8MemoryRefusal(root, runId, artifactText, readText = readTextOrNull) {
|
|
7317
|
+
const evidence = phase8MemoryEvidence(root, runId, artifactText, readText);
|
|
7318
|
+
if (evidence.ok) return null;
|
|
7319
|
+
return "locking 08-memory-impact.md requires this run to have WRITTEN a doc under .recursive/memory/: " + evidence.reason + ". Write one (memory/episodes/" + runId + ".md is always available), declare its path under `## Affected Memory Docs`, and give the doc `Source-Runs: " + runId + "` — then retry the lock.";
|
|
7320
|
+
}
|
|
7321
|
+
/**
|
|
7322
|
+
* T40 — THE LOCK-TIME ENTRY POINT: the refusal for the artifact being locked, or null.
|
|
7323
|
+
*
|
|
7324
|
+
* ⚠ THIS EXISTS SO THE GATE IS ONE LINE AT ITS CALL SITE. The decision (declared → exists → carries
|
|
7325
|
+
* this run's provenance) belongs in this module with the write surface that produces it; `lockArtifact`
|
|
7326
|
+
* should have to say only WHICH artifact it is locking, not how a memory doc is recognised. A caller
|
|
7327
|
+
* that has to reproduce the rule would be a second copy of it.
|
|
7328
|
+
*
|
|
7329
|
+
* ⚠ AND A MISSING ARTIFACT IS NOT THIS GATE'S REFUSAL. `lockArtifact` already refuses an absent
|
|
7330
|
+
* artifact before any gate can run, and returning a memory refusal for a file that does not exist
|
|
7331
|
+
* would replace "the artifact is missing" with a sentence about memory — a misleading diagnosis in
|
|
7332
|
+
* exchange for nothing.
|
|
7333
|
+
*/
|
|
7334
|
+
function phase8MemoryLockRefusal(root, runId, artifact) {
|
|
7335
|
+
if (artifact !== PHASE8_ARTIFACT) return null;
|
|
7336
|
+
const artifactText = readTextOrNull(join(root, ".recursive", "run", runId, PHASE8_ARTIFACT));
|
|
7337
|
+
if (artifactText === null) return null;
|
|
7338
|
+
return phase8MemoryRefusal(root, runId, artifactText);
|
|
7339
|
+
}
|
|
6978
7340
|
//#endregion
|
|
6979
7341
|
//#region src/run-spec.ts
|
|
6980
7342
|
/** The named evidence classes, so a reader can tell a placeholder from an unmet gate. */
|
|
@@ -7724,6 +8086,75 @@ async function askRunStartDirectly(channel, exec) {
|
|
|
7724
8086
|
}
|
|
7725
8087
|
}
|
|
7726
8088
|
//#endregion
|
|
8089
|
+
//#region src/run-id.ts
|
|
8090
|
+
/**
|
|
8091
|
+
* A RUN ID IS A NAME, NOT A PATH.
|
|
8092
|
+
*
|
|
8093
|
+
* WHY THIS MODULE EXISTS. Every consumer of a run id JOINS it onto a directory
|
|
8094
|
+
* that already carries the meaning "the run layer":
|
|
8095
|
+
*
|
|
8096
|
+
* join(root, '.recursive', 'run', runId) // runtime.ts, run.ts, handoff.ts, scratch.ts
|
|
8097
|
+
* join(repoRoot, '.worktrees', runId) // worktree.ts (a linked worktree)
|
|
8098
|
+
* 'recursive/' + runId // worktree.ts (the run's git branch)
|
|
8099
|
+
*
|
|
8100
|
+
* `join` is a PATH operation: absolute paths, drive specifiers and `..` segments
|
|
8101
|
+
* are all legal input to it, and each one silently changes what the call means.
|
|
8102
|
+
* A caller who passes `E:\tmp\rm-live-diagnostics\01-calculator-lib` is asking
|
|
8103
|
+
* for a run "on another drive"; what they get is a `mkdir` of
|
|
8104
|
+
*
|
|
8105
|
+
* <workspace>\.recursive\run\E:\tmp\rm-live-diagnostics\01-calculator-lib
|
|
8106
|
+
*
|
|
8107
|
+
* which is not drive-qualified at all — on POSIX and Windows alike the colon is
|
|
8108
|
+
* just another character in a relative component. The result is a bogus nested
|
|
8109
|
+
* folder INSIDE the workspace, created before anything can refuse it, surfacing
|
|
8110
|
+
* far away as an ENOENT-shaped runtime failure (RM5501) with the operator's
|
|
8111
|
+
* filesystem already dirty.
|
|
8112
|
+
*
|
|
8113
|
+
* SO THE RULE IS ENFORCED WHERE THE NAME ENTERS, and NOT by teaching the runtime
|
|
8114
|
+
* to accept a path. The joins in `runtime.ts` are CORRECT for a name; what was
|
|
8115
|
+
* missing was a gate on the name. Do not "fix" this back: a run on another drive
|
|
8116
|
+
* or in a worktree is reached through the session's control-plane root
|
|
8117
|
+
* (`recursive_worktree`, `00-worktree.md`) — the run layer is never relocated by
|
|
8118
|
+
* smuggling a path into the id.
|
|
8119
|
+
*
|
|
8120
|
+
* The charset below is deliberately the SAME one the read path already uses
|
|
8121
|
+
* (`live-route.ts` `DOC_SAFE_RE`) so a name this gate accepts is a name that
|
|
8122
|
+
* route can serve.
|
|
8123
|
+
*/
|
|
8124
|
+
/**
|
|
8125
|
+
* The accepted shape, as prose that can be embedded in a model-facing parameter
|
|
8126
|
+
* description and in a refusal detail, so the rule is stated once.
|
|
8127
|
+
*/
|
|
8128
|
+
const RUN_ID_RULE = "letters, digits, dot, underscore or dash only, no leading or trailing dot, no path separator, no drive specifier and no \"..\" segment";
|
|
8129
|
+
/** Directory-name charset — the read path's `DOC_SAFE_RE`, verbatim. */
|
|
8130
|
+
const RUN_ID_CHARS = /^[A-Za-z0-9._-]+$/;
|
|
8131
|
+
/**
|
|
8132
|
+
* Why a run id is refused, or `null` when it is a usable NAME.
|
|
8133
|
+
*
|
|
8134
|
+
* The returned string is the SPECIFIC problem (which rule the id broke), with no
|
|
8135
|
+
* trailing punctuation and no sentence of its own, so a caller can hand it to
|
|
8136
|
+
* `toolError('BAD_RUN_ID', …)` as the detail. `RUN_ID_RULE` states the shape.
|
|
8137
|
+
*
|
|
8138
|
+
* The order of the checks is part of the message quality: a Windows absolute
|
|
8139
|
+
* path is reported as a drive-qualified path (what the caller passed) rather
|
|
8140
|
+
* than as a separator complaint (what that path is made of).
|
|
8141
|
+
*/
|
|
8142
|
+
function runIdProblem(raw) {
|
|
8143
|
+
if (raw === "") return "runId is empty";
|
|
8144
|
+
if (raw.length > 100) return "runId is " + raw.length + " characters, over the 100 allowed";
|
|
8145
|
+
if (/^[A-Za-z]:/.test(raw)) return "runId is a Windows drive-qualified path, starting with \"" + raw.slice(0, 2) + "\"";
|
|
8146
|
+
if (raw.includes("/") || raw.includes("\\")) return "runId contains the path separator \"" + (raw.includes("/") ? "/" : "\\") + "\"";
|
|
8147
|
+
if (raw.includes(":")) return "runId contains a colon (\":\"), which is a drive and stream separator on Windows";
|
|
8148
|
+
if (raw.includes("..")) return "runId contains a \"..\" segment, which escapes the run directory";
|
|
8149
|
+
if (raw.startsWith(".")) return "runId starts with \".\", which makes it a hidden name or a relative path segment";
|
|
8150
|
+
if (raw.endsWith(".")) return "runId ends with \".\"";
|
|
8151
|
+
if (!RUN_ID_CHARS.test(raw)) {
|
|
8152
|
+
if (/\s/.test(raw)) return "runId contains a space or other whitespace character inside the name";
|
|
8153
|
+
return "runId contains a character outside the allowed set";
|
|
8154
|
+
}
|
|
8155
|
+
return null;
|
|
8156
|
+
}
|
|
8157
|
+
//#endregion
|
|
7727
8158
|
//#region src/lifecycle.ts
|
|
7728
8159
|
/**
|
|
7729
8160
|
* Transition gate validation + goal coupling (Phase C R1/R2/R6, PROPOSAL 8.4).
|
|
@@ -7982,6 +8413,64 @@ function currentPhaseArtifact(worktreeRoot, runId) {
|
|
|
7982
8413
|
return inForce !== "" ? inForce : best;
|
|
7983
8414
|
}
|
|
7984
8415
|
/**
|
|
8416
|
+
* ISSUE 2 (a) — THE RUN A GUARD CALL IS ABOUT, and the one whose tree it may read.
|
|
8417
|
+
*
|
|
8418
|
+
* THE DEFECT THIS ANSWERS, measured before the fix: `recursive_lock {runId: 'run-b', artifact:
|
|
8419
|
+
* '01-as-is.md'}` was REFUSED with `monotonic lock-order: … 00-requirements.md (DRAFT)` — run-A's blocker —
|
|
8420
|
+
* while `run-b` had `00-requirements.md` LOCKED and `01-as-is.md` DRAFT, so locking it in run-b was LEGAL.
|
|
8421
|
+
* The guard resolved the run from the FILESYSTEM (`resolveRunDir`, i.e. the active/newest run) while the
|
|
8422
|
+
* tool resolves it from `args.runId`, so the guard judged a DIFFERENT RUN than the call was about. Under
|
|
8423
|
+
* `advisory` the deny was coerced to an allow-with-warning and the tool refused on its own terms, which is
|
|
8424
|
+
* why the strict default is what made it bite.
|
|
8425
|
+
*
|
|
8426
|
+
* SO THE RULE IS: for a LOCK call that NAMES a run, the guard judges THAT RUN. It is the same choice the
|
|
8427
|
+
* tool makes, so the two layers cannot disagree about which tree the ordering rule is a property of. A
|
|
8428
|
+
* caller that names nothing (every real `write`, and a lock that relies on the active run) is unaffected:
|
|
8429
|
+
* the active run still governs, which is what the write-side rules rely on.
|
|
8430
|
+
*
|
|
8431
|
+
* ⚠ THIS IS SCOPED TO THE LOCK TOOLS DELIBERATELY, and the scope is per rule, not per convenience:
|
|
8432
|
+
*
|
|
8433
|
+
* - `lock-order` (`recursive_lock*`) — the caller's run WINS. The tool acts on `args.runId`, and the
|
|
8434
|
+
* rule is about THAT run's prerequisites, so the guard must not answer for another run. This is the
|
|
8435
|
+
* measured defect.
|
|
8436
|
+
* - `locked-write` (the write-tool family) — NOT APPLICABLE, by construction: the rule resolves no run
|
|
8437
|
+
* at all. It reads the target file's own `Status:` through the path the caller named, so there is no
|
|
8438
|
+
* run to prefer and nothing could disagree.
|
|
8439
|
+
* - `phase-order` (the write-tool family) — the ACTIVE run KEEPS WINNING, and this function does not
|
|
8440
|
+
* touch it. Two reasons, both deliberate: (1) a `write` call carries no run id — no write tool declares
|
|
8441
|
+
* one — so consulting `args.runId` here would hand a caller a way to ESCAPE the active run's ordering
|
|
8442
|
+
* by naming some other run in an argument the tool ignores; and (2) the rule's declared scope is the
|
|
8443
|
+
* run being worked in (it abstains for another run's tree, documented in `phaseOrderRule`), and moving
|
|
8444
|
+
* that scope would be a new refusal, not a consistency fix.
|
|
8445
|
+
*
|
|
8446
|
+
* ⚠ A CALLER-SUPPLIED ID IS A NAME, NEVER A PATH, and it is validated before it can point the guard at
|
|
8447
|
+
* anything: the id is trimmed the way `recursive_lock` trims it, then put through `runIdProblem` — the
|
|
8448
|
+
* SAME gate the run-id-shaped tools use, which refuses separators, drive specifiers, `..`, a colon, a
|
|
8449
|
+
* leading/trailing dot and an over-long name — and finally the resolved directory must sit UNDER this
|
|
8450
|
+
* worktree's `<root>/.recursive/run`, the containment rule `runtime.ts` applies to a run directory.
|
|
8451
|
+
*
|
|
8452
|
+
* An id that fails any of those is NOT USED: the guard falls back to the active run, exactly as it behaved
|
|
8453
|
+
* before this change. Falling back (rather than denying) is deliberate: an unusable id is a caller mistake
|
|
8454
|
+
* the tool itself refuses (`BAD_RUN_ID` / `Artifact not found`), and inventing a new guard refusal for it
|
|
8455
|
+
* would be a second, competing answer to a question `runIdProblem` already owns.
|
|
8456
|
+
*
|
|
8457
|
+
* A usable id does NOT have to name an EXISTING run: a run with no tree has no unlocked prerequisites, so
|
|
8458
|
+
* the ordering rule abstains and the LOCK TOOL still refuses the lock (it checks the artifact exists before
|
|
8459
|
+
* anything else). Requiring existence would instead re-introduce the defect in its ugliest form — a refusal
|
|
8460
|
+
* built from ANOTHER run's blockers.
|
|
8461
|
+
*/
|
|
8462
|
+
function resolveGuardRunId(name, args, worktreeRoot, activeRunId) {
|
|
8463
|
+
if (!LOCK_TOOL_NAMES.has(name) || !worktreeRoot) return activeRunId;
|
|
8464
|
+
const raw = args.runId;
|
|
8465
|
+
if (typeof raw !== "string") return activeRunId;
|
|
8466
|
+
const declared = raw.trim();
|
|
8467
|
+
if (declared === "" || runIdProblem(declared) !== null) return activeRunId;
|
|
8468
|
+
const runRoot = resolve(worktreeRoot, ".recursive", "run");
|
|
8469
|
+
const prefix = runRoot.endsWith(sep) ? runRoot : runRoot + sep;
|
|
8470
|
+
if (!resolve(join(runRoot, declared)).startsWith(prefix)) return activeRunId;
|
|
8471
|
+
return declared;
|
|
8472
|
+
}
|
|
8473
|
+
/**
|
|
7985
8474
|
* `mode` is the gate's configured posture. Its parameter default FOLLOWS the config
|
|
7986
8475
|
* default by REFERENCE (`DEFAULT_ENFORCEMENT.toolGuards`) rather than repeating the
|
|
7987
8476
|
* literal: a bare call is "the caller had no mode to hand", and the answer to that must
|
|
@@ -7996,17 +8485,20 @@ function currentPhaseArtifact(worktreeRoot, runId) {
|
|
|
7996
8485
|
function evaluateToolGuard(exec, worktreeRoot, activeRunId, mode = DEFAULT_ENFORCEMENT.toolGuards) {
|
|
7997
8486
|
const name = exec.name;
|
|
7998
8487
|
const args = exec.arguments ?? {};
|
|
7999
|
-
const runId = typeof activeRunId === "string" ? activeRunId.trim() : "";
|
|
8488
|
+
const runId = resolveGuardRunId(name, args, worktreeRoot, typeof activeRunId === "string" ? activeRunId.trim() : "");
|
|
8000
8489
|
const runDir = join(worktreeRoot, ".recursive", "run", runId);
|
|
8001
8490
|
const transition = consultTransitionGate(name, args, worktreeRoot, runId);
|
|
8002
8491
|
const activePhaseArtifact = currentPhaseArtifact(worktreeRoot, runId);
|
|
8003
|
-
return
|
|
8004
|
-
args,
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8492
|
+
return {
|
|
8493
|
+
...advisory(verdictFor(mode, evaluateToolPolicy(resolveToolPolicyForGuard(worktreeRoot, runId, activePhaseArtifact), name, args, {
|
|
8494
|
+
args,
|
|
8495
|
+
runDir,
|
|
8496
|
+
runId,
|
|
8497
|
+
worktreeRoot,
|
|
8498
|
+
activePhaseArtifact
|
|
8499
|
+
}), String(args.artifact ?? "")), transition),
|
|
8500
|
+
runId
|
|
8501
|
+
};
|
|
8010
8502
|
}
|
|
8011
8503
|
/**
|
|
8012
8504
|
* Map the policy's verdict onto the guard's decision kind: `strict` denies,
|
|
@@ -10751,14 +11243,14 @@ var RecursiveRuntime = class extends Service {
|
|
|
10751
11243
|
responseFile: join(runDir, "training-response.json")
|
|
10752
11244
|
}),
|
|
10753
11245
|
write: (relativePath, content) => {
|
|
10754
|
-
const target = join(root, relativePath);
|
|
11246
|
+
const target = join(root, ".recursive", relativePath);
|
|
10755
11247
|
mkdirSync(dirname(target), { recursive: true });
|
|
10756
11248
|
writeFileSync(target, content, "utf8");
|
|
10757
11249
|
return relativePath;
|
|
10758
11250
|
},
|
|
10759
11251
|
readText: (relativePath) => {
|
|
10760
11252
|
try {
|
|
10761
|
-
return readFileSync(join(root, relativePath), "utf8");
|
|
11253
|
+
return readFileSync(join(root, ".recursive", relativePath), "utf8");
|
|
10762
11254
|
} catch {
|
|
10763
11255
|
return null;
|
|
10764
11256
|
}
|
|
@@ -11476,6 +11968,16 @@ var RecursiveRuntime = class extends Service {
|
|
|
11476
11968
|
}
|
|
11477
11969
|
const inFlight = pendingWork(runDir);
|
|
11478
11970
|
if (inFlight.length > 0) throw new Error(toolError("PENDING_WORK", inFlight.map((p) => p.detail).join("; ")));
|
|
11971
|
+
const memoryRefusal = phase8MemoryLockRefusal(root, runId, artifact);
|
|
11972
|
+
if (memoryRefusal !== null) {
|
|
11973
|
+
try {
|
|
11974
|
+
this.blockRunToGoal(agent, runId, {
|
|
11975
|
+
code: "phase8-memory-missing",
|
|
11976
|
+
message: memoryRefusal
|
|
11977
|
+
});
|
|
11978
|
+
} catch {}
|
|
11979
|
+
throw new Error(memoryRefusal);
|
|
11980
|
+
}
|
|
11479
11981
|
const lint = await this.lintArtifact(runId, artifact, agent);
|
|
11480
11982
|
if (!lint.passed) throw new Error("Artifact " + artifact + " does not meet the phase standard, so it was not locked: " + lint.errors.join("; "));
|
|
11481
11983
|
let content = readFileSync(artifactPath, "utf8");
|
|
@@ -11874,75 +12376,6 @@ function createRecursiveStatusTool(recursive) {
|
|
|
11874
12376
|
});
|
|
11875
12377
|
}
|
|
11876
12378
|
//#endregion
|
|
11877
|
-
//#region src/run-id.ts
|
|
11878
|
-
/**
|
|
11879
|
-
* A RUN ID IS A NAME, NOT A PATH.
|
|
11880
|
-
*
|
|
11881
|
-
* WHY THIS MODULE EXISTS. Every consumer of a run id JOINS it onto a directory
|
|
11882
|
-
* that already carries the meaning "the run layer":
|
|
11883
|
-
*
|
|
11884
|
-
* join(root, '.recursive', 'run', runId) // runtime.ts, run.ts, handoff.ts, scratch.ts
|
|
11885
|
-
* join(repoRoot, '.worktrees', runId) // worktree.ts (a linked worktree)
|
|
11886
|
-
* 'recursive/' + runId // worktree.ts (the run's git branch)
|
|
11887
|
-
*
|
|
11888
|
-
* `join` is a PATH operation: absolute paths, drive specifiers and `..` segments
|
|
11889
|
-
* are all legal input to it, and each one silently changes what the call means.
|
|
11890
|
-
* A caller who passes `E:\tmp\rm-live-diagnostics\01-calculator-lib` is asking
|
|
11891
|
-
* for a run "on another drive"; what they get is a `mkdir` of
|
|
11892
|
-
*
|
|
11893
|
-
* <workspace>\.recursive\run\E:\tmp\rm-live-diagnostics\01-calculator-lib
|
|
11894
|
-
*
|
|
11895
|
-
* which is not drive-qualified at all — on POSIX and Windows alike the colon is
|
|
11896
|
-
* just another character in a relative component. The result is a bogus nested
|
|
11897
|
-
* folder INSIDE the workspace, created before anything can refuse it, surfacing
|
|
11898
|
-
* far away as an ENOENT-shaped runtime failure (RM5501) with the operator's
|
|
11899
|
-
* filesystem already dirty.
|
|
11900
|
-
*
|
|
11901
|
-
* SO THE RULE IS ENFORCED WHERE THE NAME ENTERS, and NOT by teaching the runtime
|
|
11902
|
-
* to accept a path. The joins in `runtime.ts` are CORRECT for a name; what was
|
|
11903
|
-
* missing was a gate on the name. Do not "fix" this back: a run on another drive
|
|
11904
|
-
* or in a worktree is reached through the session's control-plane root
|
|
11905
|
-
* (`recursive_worktree`, `00-worktree.md`) — the run layer is never relocated by
|
|
11906
|
-
* smuggling a path into the id.
|
|
11907
|
-
*
|
|
11908
|
-
* The charset below is deliberately the SAME one the read path already uses
|
|
11909
|
-
* (`live-route.ts` `DOC_SAFE_RE`) so a name this gate accepts is a name that
|
|
11910
|
-
* route can serve.
|
|
11911
|
-
*/
|
|
11912
|
-
/**
|
|
11913
|
-
* The accepted shape, as prose that can be embedded in a model-facing parameter
|
|
11914
|
-
* description and in a refusal detail, so the rule is stated once.
|
|
11915
|
-
*/
|
|
11916
|
-
const RUN_ID_RULE = "letters, digits, dot, underscore or dash only, no leading or trailing dot, no path separator, no drive specifier and no \"..\" segment";
|
|
11917
|
-
/** Directory-name charset — the read path's `DOC_SAFE_RE`, verbatim. */
|
|
11918
|
-
const RUN_ID_CHARS = /^[A-Za-z0-9._-]+$/;
|
|
11919
|
-
/**
|
|
11920
|
-
* Why a run id is refused, or `null` when it is a usable NAME.
|
|
11921
|
-
*
|
|
11922
|
-
* The returned string is the SPECIFIC problem (which rule the id broke), with no
|
|
11923
|
-
* trailing punctuation and no sentence of its own, so a caller can hand it to
|
|
11924
|
-
* `toolError('BAD_RUN_ID', …)` as the detail. `RUN_ID_RULE` states the shape.
|
|
11925
|
-
*
|
|
11926
|
-
* The order of the checks is part of the message quality: a Windows absolute
|
|
11927
|
-
* path is reported as a drive-qualified path (what the caller passed) rather
|
|
11928
|
-
* than as a separator complaint (what that path is made of).
|
|
11929
|
-
*/
|
|
11930
|
-
function runIdProblem(raw) {
|
|
11931
|
-
if (raw === "") return "runId is empty";
|
|
11932
|
-
if (raw.length > 100) return "runId is " + raw.length + " characters, over the 100 allowed";
|
|
11933
|
-
if (/^[A-Za-z]:/.test(raw)) return "runId is a Windows drive-qualified path, starting with \"" + raw.slice(0, 2) + "\"";
|
|
11934
|
-
if (raw.includes("/") || raw.includes("\\")) return "runId contains the path separator \"" + (raw.includes("/") ? "/" : "\\") + "\"";
|
|
11935
|
-
if (raw.includes(":")) return "runId contains a colon (\":\"), which is a drive and stream separator on Windows";
|
|
11936
|
-
if (raw.includes("..")) return "runId contains a \"..\" segment, which escapes the run directory";
|
|
11937
|
-
if (raw.startsWith(".")) return "runId starts with \".\", which makes it a hidden name or a relative path segment";
|
|
11938
|
-
if (raw.endsWith(".")) return "runId ends with \".\"";
|
|
11939
|
-
if (!RUN_ID_CHARS.test(raw)) {
|
|
11940
|
-
if (/\s/.test(raw)) return "runId contains a space or other whitespace character inside the name";
|
|
11941
|
-
return "runId contains a character outside the allowed set";
|
|
11942
|
-
}
|
|
11943
|
-
return null;
|
|
11944
|
-
}
|
|
11945
|
-
//#endregion
|
|
11946
12379
|
//#region src/recursive_init.tool.ts
|
|
11947
12380
|
/**
|
|
11948
12381
|
* PHASE 0 — SCAFFOLDING IS NOT STARTING, AND THE TOOL SAYS SO AT THE MOMENT IT MATTERS.
|
|
@@ -13245,10 +13678,12 @@ function createRecursivePreviewTool(recursive) {
|
|
|
13245
13678
|
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
13246
13679
|
* core: bootstrap the FULL canonical /.recursive/ control plane + cross-tool
|
|
13247
13680
|
* bridges byte-identically (RECURSIVE.md marker-wrapped, AGENTS.md, STATE/
|
|
13248
|
-
* DECISIONS, memory routers + shards, config/recursive-router.json, .gitignore,
|
|
13249
|
-
*
|
|
13250
|
-
*
|
|
13251
|
-
*
|
|
13681
|
+
* DECISIONS, memory routers + shards, config/recursive-router.json, .gitignore),
|
|
13682
|
+
* plus the agent/session-start Stage B (new vs resume) workspace-scoped to the
|
|
13683
|
+
* session's control-plane root (R1).
|
|
13684
|
+
*
|
|
13685
|
+
* ⚠ NO `.recursive/scripts/` IS CREATED, and a legacy one is removed once it is empty — see the block in
|
|
13686
|
+
* the scaffold below for the measurement that decided it.
|
|
13252
13687
|
*
|
|
13253
13688
|
* Templates + bodies + runtime scripts are SHIPPED package files under
|
|
13254
13689
|
* references/ (never inlined TS string literals) and resolved relative to this
|
|
@@ -13485,11 +13920,13 @@ function bootstrapScaffold(root) {
|
|
|
13485
13920
|
".recursive/memory/skills/patterns/.gitkeep",
|
|
13486
13921
|
".recursive/run/.gitkeep"
|
|
13487
13922
|
]) noteFile(rel, "");
|
|
13488
|
-
noteDir(".recursive/scripts");
|
|
13489
13923
|
{
|
|
13490
13924
|
const scriptsDir = join(recursiveRoot, "scripts");
|
|
13491
13925
|
if (existsSync(scriptsDir)) {
|
|
13492
13926
|
for (const name of readdirSync(scriptsDir)) if (name.endsWith(".py") || name.endsWith(".ps1")) rmSync(join(scriptsDir, name), { force: true });
|
|
13927
|
+
try {
|
|
13928
|
+
if (readdirSync(scriptsDir).length === 0) rmdirSync(scriptsDir);
|
|
13929
|
+
} catch {}
|
|
13493
13930
|
}
|
|
13494
13931
|
}
|
|
13495
13932
|
noteFile(".recursive/RECURSIVE.md", "# RECURSIVE.md\n");
|
|
@@ -14735,15 +15172,17 @@ function runToolGuard(recursive, exec, root, runId) {
|
|
|
14735
15172
|
const guardMode = recursive.enforcementConfig.toolGuards;
|
|
14736
15173
|
const decision = evaluateToolGuard(exec, root, runId, guardMode);
|
|
14737
15174
|
const coerced = coerceAskToDecision(decision, guardMode);
|
|
15175
|
+
const evaluatedRunId = decision.runId ?? runId;
|
|
14738
15176
|
const final = coerced === decision ? decision : {
|
|
14739
15177
|
...coerced,
|
|
14740
15178
|
rule: decision.rule,
|
|
14741
|
-
transition: decision.transition
|
|
15179
|
+
transition: decision.transition,
|
|
15180
|
+
runId: evaluatedRunId
|
|
14742
15181
|
};
|
|
14743
15182
|
if (root) {
|
|
14744
15183
|
const record = {
|
|
14745
15184
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14746
|
-
runId,
|
|
15185
|
+
runId: evaluatedRunId,
|
|
14747
15186
|
tool: exec?.name ?? "",
|
|
14748
15187
|
kind: final.kind,
|
|
14749
15188
|
rule: final.rule ?? "none"
|
|
@@ -14757,6 +15196,54 @@ function runToolGuard(recursive, exec, root, runId) {
|
|
|
14757
15196
|
}
|
|
14758
15197
|
return final;
|
|
14759
15198
|
}
|
|
15199
|
+
/**
|
|
15200
|
+
* ISSUE 1 — THE GOAL BLOCK, FROM THE LAYER THAT REFUSED.
|
|
15201
|
+
*
|
|
15202
|
+
* WHERE THIS BELONGS, decided from the code rather than assumed: the GUARD CANNOT DO THIS ITSELF.
|
|
15203
|
+
* `evaluateToolGuard` is a pure policy layer — it takes an exec, a worktree root, a run id and a mode, and
|
|
15204
|
+
* it has no goals service, no live agent and no runtime handle; it is also called from a dry-run preview
|
|
15205
|
+
* (`src/recursive_preview.tool.ts`), where a side effect would be a lie about a call that never happened.
|
|
15206
|
+
* The ONE place where a guard refusal becomes real is the `tools/pre-execute` listener below: it holds the
|
|
15207
|
+
* runtime (which owns `blockRunToGoal` and the late-attached goals service), the live agent from the exec
|
|
15208
|
+
* payload, and the decision itself — and it is the same boundary that already renders the refusal's ask
|
|
15209
|
+
* into the caller's text (FU-7). So this is called there, and nowhere else.
|
|
15210
|
+
*
|
|
15211
|
+
* WHY IT IS NEEDED AT ALL. `lockArtifact` blocks the run's goal when its OWN ordering check refuses
|
|
15212
|
+
* (`runtime.ts`, the `Prerequisite blockers:` branch). Under the strict default the guard refuses an
|
|
15213
|
+
* out-of-order lock BEFORE DISPATCH, so `lockArtifact` never runs, its block never happens, and the run was
|
|
15214
|
+
* told it was blocked while the goal machinery was not — the goal stayed armed and kept driving rounds
|
|
15215
|
+
* through a refused gate.
|
|
15216
|
+
*
|
|
15217
|
+
* ⚠ WHY THIS CANNOT DOUBLE-BLOCK. The two block sites are on MUTUALLY EXCLUSIVE branches of one call:
|
|
15218
|
+
* this one runs only when the guard DENIED (so the tool is never dispatched), and the tool's own block runs
|
|
15219
|
+
* only when the guard let the call through to `lockArtifact`. One refusal, one dispatch decision, one
|
|
15220
|
+
* block. A repeat of the SAME refused call re-enters this branch, and the second block is refused by the
|
|
15221
|
+
* goal service itself (`block` requires an ACTIVE goal; an already-blocked goal is not active), which is
|
|
15222
|
+
* swallowed here exactly as the tool path swallows it — the goal stays blocked, and it is not blocked
|
|
15223
|
+
* twice.
|
|
15224
|
+
*
|
|
15225
|
+
* ⚠ THE TRIGGER IS THE ASK, NOT THE RULE LABEL — the same trigger FU-7 uses, for the same reason: an ask is
|
|
15226
|
+
* present exactly when the refusal was DECIDED FROM REAL ORDERING BLOCKERS (`PolicyDecision.blockers` read
|
|
15227
|
+
* from disk), which includes a policy FILE whose `recursive_lock*` deny carries no label (its `rule` reads
|
|
15228
|
+
* `none`). Gating on the label instead would silently skip the goal block in every repo that ships a policy
|
|
15229
|
+
* file — the shipped default here.
|
|
15230
|
+
*
|
|
15231
|
+
* ⚠ AND IT IS THE LOCK ORDERING REFUSAL ONLY. The phase-order WRITE rule refuses a write ahead of the
|
|
15232
|
+
* active phase, and it has NO tool-layer counterpart that blocks a goal — `lockArtifact` is the only
|
|
15233
|
+
* tool-side blocker in the plugin. Blocking a goal on it would be a NEW behaviour, not the consistency this
|
|
15234
|
+
* fix is for: the defect was one refusal with two layers disagreeing, not a rule that should start
|
|
15235
|
+
* blocking.
|
|
15236
|
+
*/
|
|
15237
|
+
function blockGoalOnGuardRefusal(recursive, exec, decision, activeRunId) {
|
|
15238
|
+
if (decision.kind !== "deny" || decision.ask === void 0) return;
|
|
15239
|
+
const agent = exec?.agent ?? null;
|
|
15240
|
+
try {
|
|
15241
|
+
recursive.blockRunToGoal(agent, decision.runId ?? activeRunId, {
|
|
15242
|
+
code: "prerequisite-blockers",
|
|
15243
|
+
message: decision.reason ?? "the lock was refused: its prerequisites are unmet"
|
|
15244
|
+
});
|
|
15245
|
+
} catch {}
|
|
15246
|
+
}
|
|
14760
15247
|
function apply(ctx, config) {
|
|
14761
15248
|
if (config?.shellOnly) return;
|
|
14762
15249
|
ctx.effect(function* () {
|
|
@@ -14907,10 +15394,13 @@ function apply(ctx, config) {
|
|
|
14907
15394
|
kind: "deny",
|
|
14908
15395
|
reason: "the tool guard produced no decision"
|
|
14909
15396
|
};
|
|
14910
|
-
if (final.kind === "deny")
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
15397
|
+
if (final.kind === "deny") {
|
|
15398
|
+
blockGoalOnGuardRefusal(recursive, exec, final, runId);
|
|
15399
|
+
return final.ask === void 0 ? final : {
|
|
15400
|
+
...final,
|
|
15401
|
+
reason: final.reason + " " + renderGateBlockAsk(final.ask)
|
|
15402
|
+
};
|
|
15403
|
+
}
|
|
14914
15404
|
if (final.kind === "allow" && final.warn) console.warn("[recursive] tool guard (" + recursive.enforcementConfig.toolGuards + ") allowed this call: " + final.warn);
|
|
14915
15405
|
return typeof next === "function" ? next() : { kind: "allow" };
|
|
14916
15406
|
}));
|
|
@@ -15028,4 +15518,4 @@ function apply(ctx, config) {
|
|
|
15028
15518
|
});
|
|
15029
15519
|
}
|
|
15030
15520
|
//#endregion
|
|
15031
|
-
export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, DEFAULT_ENFORCEMENT_MODE, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, actionRecordStatus, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, buildWorkSlice, builtInToolPolicy, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, contractDigest, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, currentPhaseArtifact, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldDiagnostics, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, parseReplyVerdict, pendingWork, phaseIndex, phasePosition, probeCapabilities, readReceipt, readRepairFromReply, readRepairFromStructured, readVerdictFromReply, readVerdictFromStructured, receiptPath, referencesFromResult, registerRecursiveSkill, remainingDepthFor, renderPhaseTail, renderRecursivePolicy, renderStableContract, renderTaskHistory, replyPath, resetFoldCache, resolveEnforcementConfig, resolveRole, resolveRunDir, resolveToolPolicyForGuard, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, tamperCandidatePath, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|
|
15521
|
+
export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, DEFAULT_ENFORCEMENT_MODE, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, actionRecordStatus, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, buildWorkSlice, builtInToolPolicy, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, contractDigest, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, currentPhaseArtifact, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldDiagnostics, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, parseReplyVerdict, pendingWork, phaseIndex, phasePosition, probeCapabilities, readReceipt, readRepairFromReply, readRepairFromStructured, readVerdictFromReply, readVerdictFromStructured, receiptPath, referencesFromResult, registerRecursiveSkill, remainingDepthFor, renderPhaseTail, renderRecursivePolicy, renderStableContract, renderTaskHistory, replyPath, resetFoldCache, resolveEnforcementConfig, resolveGuardRunId, resolveRole, resolveRunDir, resolveToolPolicyForGuard, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, tamperCandidatePath, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|