@try-works/dsh-recursive-mode 0.6.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/lib/index.js +199 -25
- package/package.json +1 -1
- package/src/runtime.ts +1959 -1945
package/lib/index.js
CHANGED
|
@@ -2071,34 +2071,55 @@ const SECTION_MAP = {
|
|
|
2071
2071
|
* field the plane's own linter already requires, so the artifact shape stays canonical.
|
|
2072
2072
|
*/
|
|
2073
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";
|
|
2074
2116
|
const PHASE8_MEMORY_WRITE_RULE = {
|
|
2075
2117
|
artifact: PHASE8_MEMORY_ARTIFACT,
|
|
2076
|
-
plane:
|
|
2077
|
-
section:
|
|
2078
|
-
provenanceField:
|
|
2118
|
+
plane: MEMORY_PLANE_PREFIX,
|
|
2119
|
+
section: PHASE8_MEMORY_SECTION,
|
|
2120
|
+
provenanceField: MEMORY_PROVENANCE_FIELD,
|
|
2079
2121
|
alwaysAvailable: ".recursive/memory/episodes/<run-id>.md",
|
|
2080
|
-
kinds: Object.keys(
|
|
2081
|
-
domain: {
|
|
2082
|
-
dir: ".recursive/memory/domains",
|
|
2083
|
-
type: "domain"
|
|
2084
|
-
},
|
|
2085
|
-
pattern: {
|
|
2086
|
-
dir: ".recursive/memory/patterns",
|
|
2087
|
-
type: "pattern"
|
|
2088
|
-
},
|
|
2089
|
-
incident: {
|
|
2090
|
-
dir: ".recursive/memory/incidents",
|
|
2091
|
-
type: "incident"
|
|
2092
|
-
},
|
|
2093
|
-
episode: {
|
|
2094
|
-
dir: ".recursive/memory/episodes",
|
|
2095
|
-
type: "episode"
|
|
2096
|
-
},
|
|
2097
|
-
skill: {
|
|
2098
|
-
dir: ".recursive/memory/skills/patterns",
|
|
2099
|
-
type: "pattern"
|
|
2100
|
-
}
|
|
2101
|
-
}),
|
|
2122
|
+
kinds: Object.keys(MEMORY_DOC_LOCATIONS),
|
|
2102
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.",
|
|
2103
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."
|
|
2104
2125
|
};
|
|
@@ -7173,6 +7194,149 @@ function renderMemoryMetadata(input) {
|
|
|
7173
7194
|
if (input.parent !== void 0 && input.parent.trim() !== "") lines.push("Parent: `" + input.parent.replace(/`/g, "'") + "`");
|
|
7174
7195
|
return lines.join("\n") + "\n";
|
|
7175
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
|
+
}
|
|
7176
7340
|
//#endregion
|
|
7177
7341
|
//#region src/run-spec.ts
|
|
7178
7342
|
/** The named evidence classes, so a reader can tell a placeholder from an unmet gate. */
|
|
@@ -11804,6 +11968,16 @@ var RecursiveRuntime = class extends Service {
|
|
|
11804
11968
|
}
|
|
11805
11969
|
const inFlight = pendingWork(runDir);
|
|
11806
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
|
+
}
|
|
11807
11981
|
const lint = await this.lintArtifact(runId, artifact, agent);
|
|
11808
11982
|
if (!lint.passed) throw new Error("Artifact " + artifact + " does not meet the phase standard, so it was not locked: " + lint.errors.join("; "));
|
|
11809
11983
|
let content = readFileSync(artifactPath, "utf8");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@try-works/dsh-recursive-mode",
|
|
3
3
|
"description": "recursive-mode workflow as a DeepSeek Harness bundle: RecursiveRuntime service + 13 recursive_* tools (recursive_status, recursive_init, recursive_lock, recursive_lint, recursive_closeout, recursive_scratch, recursive_worktree, recursive_phase, recursive_audit_team, recursive_review, recursive_delegate, recursive_ask, recursive_preview)",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|