@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.
Files changed (3) hide show
  1. package/lib/index.js +199 -25
  2. package/package.json +1 -1
  3. 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: ".recursive/memory/",
2077
- section: "Affected Memory Docs",
2078
- provenanceField: "Source-Runs",
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.0",
4
+ "version": "0.6.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",