@try-works/dsh-recursive-mode 0.4.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -8
- package/lib/config.d.ts +14 -0
- package/lib/enforcement.d.ts +118 -0
- package/lib/guard-log.d.ts +7 -0
- package/lib/index.js +568 -133
- package/lib/memory.d.ts +35 -2
- package/lib/phase-rules.d.ts +102 -0
- package/lib/policy-globs.d.ts +24 -0
- package/lib/recursive_ask.tool.d.ts +37 -0
- package/lib/runtime.d.ts +1 -1
- 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/scripts/test-recursive-mode-smoke.ts +11 -1
- package/src/bootstrap.ts +30 -14
- package/src/config.ts +11 -4
- package/src/enforcement.ts +202 -15
- package/src/guard-log.ts +7 -0
- package/src/index.ts +795 -700
- package/src/memory.ts +50 -9
- package/src/phase-rules.ts +162 -1
- package/src/policy-globs.ts +64 -8
- package/src/recursive_ask.tool.ts +48 -0
- package/src/recursive_lock.tool.ts +8 -2
- package/src/runtime.ts +7 -4
- package/src/training.ts +634 -6
package/lib/memory.d.ts
CHANGED
|
@@ -18,7 +18,19 @@
|
|
|
18
18
|
* so the result is reproducible from the inputs alone.
|
|
19
19
|
*/
|
|
20
20
|
import { type FeedbackBook } from './memory-feedback.ts';
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* ⚠ `training` IS IN THIS LIST BECAUSE IT IS THE KIND THE PLUGIN'S OWN TRAINING PATH WRITES.
|
|
23
|
+
*
|
|
24
|
+
* The phase-8 trigger (`training.ts`) writes `memory/training/<task-type>.md` and advertises that path in
|
|
25
|
+
* `memory/MEMORY.md`; the shipped router (`references/bodies/memory-router.md`) tells an agent to load "the
|
|
26
|
+
* relevant docs under `/.recursive/memory/training/`". With `training` absent from this list the loader
|
|
27
|
+
* COULD NOT SEE THE SHARDS THE TRIGGER WROTE — the writer's own output was unreachable by the reader, so
|
|
28
|
+
* "the plane's next run scores it" (README §10) was not true of exactly the shards training produces.
|
|
29
|
+
*
|
|
30
|
+
* `incidents/` and `archive/` stay out deliberately: `archive/` is historical by the router's own definition,
|
|
31
|
+
* and widening retrieval to `incidents/` is a separate ranking decision that this change does not make.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MEMORY_KINDS: readonly ["domains", "patterns", "episodes", "training", "skills"];
|
|
22
34
|
export type MemoryKind = (typeof MEMORY_KINDS)[number];
|
|
23
35
|
/** One memory note: a titled section of a memory document. */
|
|
24
36
|
export interface MemoryEntry {
|
|
@@ -95,7 +107,28 @@ export declare const MEMORY_PHASE_MATCH_WEIGHT = 2;
|
|
|
95
107
|
*/
|
|
96
108
|
export declare function entryAppliesTo(entry: MemoryEntry): string[];
|
|
97
109
|
/** The registry the loader starts from: the router, not the plane. */
|
|
98
|
-
export declare const MEMORY_INDEX_FILE = "memory/MEMORY.md";
|
|
110
|
+
export declare const MEMORY_INDEX_FILE = ".recursive/memory/MEMORY.md";
|
|
111
|
+
/**
|
|
112
|
+
* Where the plane may live under a workspace root, in PREFERENCE order.
|
|
113
|
+
*
|
|
114
|
+
* ⚠ THE MEASURED DEFECT THIS FIXES. `defaultMemoryList` used to join `memory/<kind>/` straight onto the
|
|
115
|
+
* root it was handed — i.e. `<root>/memory/` — and that directory EXISTS IN NO REAL WORKSPACE. `bootstrap.ts`
|
|
116
|
+
* scaffolds the plane at `<root>/.recursive/memory/`, `ts-lint.ts` lints it there, and the review bundle reads
|
|
117
|
+
* it there; this loader alone looked beside it. Measured live on a workspace whose `.recursive/memory/` was
|
|
118
|
+
* scaffolded and whose `<root>/memory/` did not exist: `selectMemory` reported "the memory plane is empty"
|
|
119
|
+
* over a plane that was there, every phase of every run — and the training shards `training.ts` writes were
|
|
120
|
+
* therefore unreachable by the loader that is supposed to score them.
|
|
121
|
+
*
|
|
122
|
+
* ⚠ PREFER, THEN FALL BACK — NEVER MERGE. This is the rule `readFeedback` already follows for its own moved
|
|
123
|
+
* sidecar (`memory-feedback.ts`: `FEEDBACK_FILE` then `LEGACY_FEEDBACK_FILE`): the current location wins, and
|
|
124
|
+
* the earlier one is consulted ONLY when the current one yields nothing, because two snapshots of one shard
|
|
125
|
+
* added together would count a shard twice and a duplicated shard would outrank a real one.
|
|
126
|
+
*
|
|
127
|
+
* ⚠ AND IT MAKES THE CALLER'S CONVENTION IRRELEVANT. Passing the workspace root resolves
|
|
128
|
+
* `<root>/.recursive/memory/`; passing `.recursive` itself resolves through the second entry. Both are
|
|
129
|
+
* accepted, which is what README §6 means by "using `.recursive` as the root is selected again".
|
|
130
|
+
*/
|
|
131
|
+
export declare const MEMORY_PLANE_BASES: readonly [".recursive/memory", "memory"];
|
|
99
132
|
/** Progressive disclosure defaults, matching the parent (`--max-docs` 3, `--max-items` 10). */
|
|
100
133
|
export declare const MAX_MEMORY_DOCS = 3;
|
|
101
134
|
export declare const MAX_MEMORY_ITEMS = 10;
|
package/lib/phase-rules.d.ts
CHANGED
|
@@ -10,6 +10,100 @@ export declare const DIFF_AUDITED_FILES: Set<string>;
|
|
|
10
10
|
export declare const TRACEABILITY_REQUIRED_FILES: Set<string>;
|
|
11
11
|
export declare const AUDIT_REQUIRED_HEADINGS: string[];
|
|
12
12
|
export declare const DIFF_BASIS_FIELDS: string[];
|
|
13
|
+
/**
|
|
14
|
+
* T40 — `.recursive/memory/`, the plane THIS plugin writes, and the phase-8 step that was prose.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ WHY THIS EXISTS, MEASURED. Three completed runs in a live workspace left `.recursive/memory/`
|
|
17
|
+
* exactly as `bootstrap.ts` scaffolded it: `MEMORY.md` and the skill docs were still the
|
|
18
|
+
* bootstrap-created placeholders, while run 03's `08-memory-impact.md` had its
|
|
19
|
+
* "Write the durable ones to memory, with provenance" box TICKED and its `Inputs` line naming
|
|
20
|
+
* ANOTHER plugin's store (`memory_search` / `memory_status` is dsh-memory, not this plugin). So the
|
|
21
|
+
* phase declared its memory step done against a system this plugin does not own, and the plugin's
|
|
22
|
+
* own learning never activated — the owner's words: *"the agent should write to .recursive/memory/
|
|
23
|
+
* in phase 8, that's a hard requirement that needs to be enforced"*.
|
|
24
|
+
*
|
|
25
|
+
* ⚠ THE FIX IS A FACT, NOT A STRONGER SENTENCE. A prose step is ticked by the agent that would have
|
|
26
|
+
* had to do it, which is exactly what happened. What follows is the same requirement in the form the
|
|
27
|
+
* workflow can CHECK: a path under the plane, declared in the artifact, whose own text on disk
|
|
28
|
+
* carries this run's provenance (`Source-Runs`). "The run wrote its durable memory" then stops being
|
|
29
|
+
* a claim about the run's intentions and becomes a claim about files.
|
|
30
|
+
*
|
|
31
|
+
* ⚠ WHY IT IS NOT IN `SECTION_MAP`, deliberately: that map is byte-parity with the canonical
|
|
32
|
+
* linter's `get_artifact_required_sections` (`tests/phase-rules.parity.spec.ts` pins all twelve
|
|
33
|
+
* lists), and a section added there would be a parity break dressed up as a feature. The requirement
|
|
34
|
+
* rides on a section the canonical template ALREADY scaffolds — `## Affected Memory Docs` — plus a
|
|
35
|
+
* field the plane's own linter already requires, so the artifact shape stays canonical.
|
|
36
|
+
*/
|
|
37
|
+
export declare const PHASE8_MEMORY_ARTIFACT = "08-memory-impact.md";
|
|
38
|
+
/** The plane, repo-relative. The trailing slash is what every guard here tests for. */
|
|
39
|
+
export declare const MEMORY_PLANE_PREFIX = ".recursive/memory/";
|
|
40
|
+
/** The canonical section the declaration goes in — present in every scaffolded phase-8 artifact. */
|
|
41
|
+
export declare const PHASE8_MEMORY_SECTION = "Affected Memory Docs";
|
|
42
|
+
/**
|
|
43
|
+
* Where a durable doc may be filed, and the `Type` the memory-plane linter requires for it.
|
|
44
|
+
*
|
|
45
|
+
* ⚠ EVERY `dir` IS THE REAL PLANE, `.recursive/memory/…`, AND NOT A `memory/…` RELATIVE FORM. Measured:
|
|
46
|
+
* `bootstrap.ts` scaffolds the plane under `.recursive/`, `ts-lint.ts` lints it there, and the phase-8
|
|
47
|
+
* artifact cites it there — while the training trigger's own write seam joins its `memory/…` paths onto
|
|
48
|
+
* the workspace root, i.e. `<root>/memory/`, a directory that exists in no real workspace (the reader
|
|
49
|
+
* side of that defect was fixed in `memory.ts`; the writer side is `runtime.ts`'s seam and is reported,
|
|
50
|
+
* not edited). This table names the location a WRITE must land in, so it names the plane.
|
|
51
|
+
*
|
|
52
|
+
* ⚠ `skill` SHARES `pattern`'s Type ON PURPOSE: `/.recursive/memory/skills/patterns/` is where a
|
|
53
|
+
* promoted skill lesson ships (`bootstrap.ts` scaffolds three docs there), and the linter's allowed
|
|
54
|
+
* Types are `index|domain|pattern|incident|episode` — there is no `skill` Type to declare.
|
|
55
|
+
*/
|
|
56
|
+
export declare const MEMORY_DOC_LOCATIONS: {
|
|
57
|
+
readonly domain: {
|
|
58
|
+
readonly dir: ".recursive/memory/domains";
|
|
59
|
+
readonly type: "domain";
|
|
60
|
+
};
|
|
61
|
+
readonly pattern: {
|
|
62
|
+
readonly dir: ".recursive/memory/patterns";
|
|
63
|
+
readonly type: "pattern";
|
|
64
|
+
};
|
|
65
|
+
readonly incident: {
|
|
66
|
+
readonly dir: ".recursive/memory/incidents";
|
|
67
|
+
readonly type: "incident";
|
|
68
|
+
};
|
|
69
|
+
readonly episode: {
|
|
70
|
+
readonly dir: ".recursive/memory/episodes";
|
|
71
|
+
readonly type: "episode";
|
|
72
|
+
};
|
|
73
|
+
readonly skill: {
|
|
74
|
+
readonly dir: ".recursive/memory/skills/patterns";
|
|
75
|
+
readonly type: "pattern";
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
export type MemoryDocKind = keyof typeof MEMORY_DOC_LOCATIONS;
|
|
79
|
+
/** The field that makes a doc THIS run's. It is the phase-8 gate's entire discriminator. */
|
|
80
|
+
export declare const MEMORY_PROVENANCE_FIELD = "Source-Runs";
|
|
81
|
+
/** Where a run that believes it learned nothing can always record what the run was and what it cost. */
|
|
82
|
+
export declare const MEMORY_ALWAYS_AVAILABLE = ".recursive/memory/episodes/<run-id>.md";
|
|
83
|
+
/**
|
|
84
|
+
* The phase-8 obligation, as data — so the pre-step reminder, the `recursive_phase` payload and the
|
|
85
|
+
* lock-time gate all describe ONE rule instead of three paraphrases of it.
|
|
86
|
+
*/
|
|
87
|
+
export interface Phase8MemoryWriteRule {
|
|
88
|
+
/** The artifact that must declare the write. */
|
|
89
|
+
artifact: string;
|
|
90
|
+
/** The plane the write must land in, repo-relative. */
|
|
91
|
+
plane: string;
|
|
92
|
+
/** The section the declaration goes in. */
|
|
93
|
+
section: string;
|
|
94
|
+
/** The field a doc must carry for the write to count as THIS run's. */
|
|
95
|
+
provenanceField: string;
|
|
96
|
+
/** The doc a run with nothing else to record can always write. */
|
|
97
|
+
alwaysAvailable: string;
|
|
98
|
+
kinds: readonly MemoryDocKind[];
|
|
99
|
+
/** One sentence per line the pre-step reminder injects. */
|
|
100
|
+
summary: string;
|
|
101
|
+
/** The full instruction, for a caller that asks for the phase's rules. */
|
|
102
|
+
instruction: string;
|
|
103
|
+
}
|
|
104
|
+
export declare const PHASE8_MEMORY_WRITE_RULE: Phase8MemoryWriteRule;
|
|
105
|
+
/** The memory-write rule for an artifact, or null when that phase owes no memory write. */
|
|
106
|
+
export declare function phase8MemoryWriteRuleFor(fileName: string): Phase8MemoryWriteRule | null;
|
|
13
107
|
/**
|
|
14
108
|
* get_artifact_required_sections(file_name, workflow_profile): canonical-parity
|
|
15
109
|
* required section headings for a phase artifact. Defaults to TODO + Coverage
|
|
@@ -41,6 +135,14 @@ export interface PhaseRules {
|
|
|
41
135
|
audited: boolean;
|
|
42
136
|
tdd: boolean;
|
|
43
137
|
qa: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* T40 — the memory-write obligation for this phase, or null when it owes none.
|
|
140
|
+
*
|
|
141
|
+
* ⚠ IT IS PART OF THIS STRUCTURE, not a parallel table, because `runtime.phaseRules` spreads this
|
|
142
|
+
* object straight into the `recursive_phase` payload and the pre-step reminder is built from the
|
|
143
|
+
* same source: a rule that lives here is a rule the agent is actually told, in one place.
|
|
144
|
+
*/
|
|
145
|
+
memoryWrite: Phase8MemoryWriteRule | null;
|
|
44
146
|
}
|
|
45
147
|
export declare function phaseRulesFor(fileName: string, workflowProfile?: string): PhaseRules;
|
|
46
148
|
/**
|
package/lib/policy-globs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PrerequisiteBlocker } from './lock.ts';
|
|
1
2
|
/** The three verdicts a rule may carry. */
|
|
2
3
|
export type Verdict = 'allow' | 'deny' | 'ask';
|
|
3
4
|
/**
|
|
@@ -10,11 +11,28 @@ export interface Decision {
|
|
|
10
11
|
kind: Verdict;
|
|
11
12
|
reason?: string;
|
|
12
13
|
rule?: string;
|
|
14
|
+
/**
|
|
15
|
+
* THE FACTS THE PREDICATE DECIDED FROM, when it read any. The lock-order rule resolves the
|
|
16
|
+
* artifact's prerequisites from disk, and the layer that turns this decision into a refusal
|
|
17
|
+
* needs those same facts to build the caller's recovery options. Carrying them is what makes
|
|
18
|
+
* "the guard already has them" true: a second `getPrerequisiteBlockers` call from the denial
|
|
19
|
+
* would re-read a run tree that is on disk and unlocked, and could answer differently.
|
|
20
|
+
*
|
|
21
|
+
* Absent for every rule that decides on its pattern alone, and absent when the predicate read
|
|
22
|
+
* nothing — so its presence means "this refusal was decided from these blockers", not "the
|
|
23
|
+
* policy mentions blockers".
|
|
24
|
+
*/
|
|
25
|
+
blockers?: readonly PrerequisiteBlocker[];
|
|
13
26
|
}
|
|
14
27
|
/**
|
|
15
28
|
* Extra facts a rule predicate may need. `args` is always the tool call's
|
|
16
29
|
* arguments; the run coordinates are present only when the caller has them
|
|
17
30
|
* (a guard call from a real session does; a pure policy unit test need not).
|
|
31
|
+
*
|
|
32
|
+
* ⚠ `runId` IS NOT DECORATION: the lock-order rule puts it in the refusal (`[run: <id>]`), so a refusal
|
|
33
|
+
* names the run whose tree it was decided from. It is the run the guard RESOLVED for this call — the run
|
|
34
|
+
* the call named when it named a usable one, otherwise the active run (see `resolveGuardRunId` in
|
|
35
|
+
* `enforcement.ts`) — never a second, independently-derived answer.
|
|
18
36
|
*/
|
|
19
37
|
export interface ToolPolicyContext {
|
|
20
38
|
args: Record<string, unknown>;
|
|
@@ -49,6 +67,12 @@ export interface ToolPolicyContext {
|
|
|
49
67
|
export interface ToolPolicyPredicateMatch {
|
|
50
68
|
verdict: Verdict;
|
|
51
69
|
detail?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The blockers the predicate READ, when it read any (see `Decision.blockers`). Optional and
|
|
72
|
+
* additive: a predicate that decided from something else returns none, and the engine's
|
|
73
|
+
* verdict is unchanged either way.
|
|
74
|
+
*/
|
|
75
|
+
blockers?: readonly PrerequisiteBlocker[];
|
|
52
76
|
}
|
|
53
77
|
export type ToolPolicyPredicate = (id: string, args: Record<string, unknown>, ctx: ToolPolicyContext) => ToolPolicyPredicateMatch | null;
|
|
54
78
|
export interface ToolPolicyRule {
|
|
@@ -58,6 +58,43 @@ export declare class AskValidationError extends Error {
|
|
|
58
58
|
export declare function validateAskQuestion(question: AskQuestion): AskQuestion;
|
|
59
59
|
/** Build the question for a gate, validated. */
|
|
60
60
|
export declare function buildAskQuestion(gateId: AskGateId): AskQuestion;
|
|
61
|
+
/**
|
|
62
|
+
* FU-7 — THE GATE-BLOCK REFUSAL PAYLOAD, BUILT IN ONE PLACE.
|
|
63
|
+
*
|
|
64
|
+
* A blocked lock is refused by TWO layers. The TOOL refuses when `lockArtifact` throws
|
|
65
|
+
* `Prerequisite blockers:`; the GUARD refuses pre-dispatch when the lock-order rule fires
|
|
66
|
+
* (`monotonic lock-order`), and under the strict default that is the layer the caller meets
|
|
67
|
+
* first. Both must hand the caller the SAME choice, so both build it HERE.
|
|
68
|
+
*
|
|
69
|
+
* ⚠ WHY NOT TWO LITERALS. `fix | reopen | abandon` is the human's way out of a blocked lock,
|
|
70
|
+
* and it existed in exactly one call site (`recursive_lock.tool.ts`). The guard's refusal moved
|
|
71
|
+
* to the default path, and a second hand-written copy of the options there would be a second
|
|
72
|
+
* answer to "what can a person do about this?": the day the options change, one of the two
|
|
73
|
+
* refusals keeps offering the old set and nothing fails. One builder, called from both.
|
|
74
|
+
*
|
|
75
|
+
* `blocked` is the refusal's OWN sentence — the guard's rule text or the tool's exception
|
|
76
|
+
* message — carried as data so the payload says what it is about without a reader having to
|
|
77
|
+
* match it against the text beside it.
|
|
78
|
+
*/
|
|
79
|
+
export interface GateBlockAsk extends AskQuestion {
|
|
80
|
+
gate: 'gate-block';
|
|
81
|
+
artifact: string;
|
|
82
|
+
blocked: string;
|
|
83
|
+
}
|
|
84
|
+
/** The payload, as the refusal carries it. Validated through `buildAskQuestion`. */
|
|
85
|
+
export declare function buildGateBlockAsk(artifact: string, blocked: string): GateBlockAsk;
|
|
86
|
+
/**
|
|
87
|
+
* FU-7 — THE OPTIONS AS TEXT, DERIVED FROM THE PAYLOAD rather than restated.
|
|
88
|
+
*
|
|
89
|
+
* WHY A RENDERER IS NEEDED AT ALL: the harness renders a `tools/pre-execute` denial as
|
|
90
|
+
* `Error: <reason>` and drops every other field of the decision (measured in
|
|
91
|
+
* `packages/core/tools`: `content: [{ type: 'text', text: 'Error: ' + denialReason }]`), so an
|
|
92
|
+
* ask that rode along as a SIBLING field would reach the model as nothing at all — which is
|
|
93
|
+
* exactly how a strict-by-default guard made the recovery path unreachable. The refusal
|
|
94
|
+
* therefore renders the payload into the text it hands back, and it renders THIS object, so
|
|
95
|
+
* the visible sentence and the structured payload cannot disagree.
|
|
96
|
+
*/
|
|
97
|
+
export declare function renderGateBlockAsk(ask: GateBlockAsk): string;
|
|
61
98
|
/**
|
|
62
99
|
* PHASE 0 — build the question for ANY accepted gate, including the run-start gate.
|
|
63
100
|
*
|
package/lib/runtime.d.ts
CHANGED
|
@@ -743,7 +743,7 @@ export declare class RecursiveRuntime extends Service {
|
|
|
743
743
|
code: string;
|
|
744
744
|
message: string;
|
|
745
745
|
}): boolean;
|
|
746
|
-
/** Phase C R7: resolve the enforcement config (strict|advisory, default
|
|
746
|
+
/** Phase C R7: resolve the enforcement config (strict|advisory, default strict). */
|
|
747
747
|
get enforcementConfig(): EnforcementConfig;
|
|
748
748
|
setEnforcementConfig(config: unknown): EnforcementConfig;
|
|
749
749
|
}
|
package/lib/training.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MemoryDocKind } from './phase-rules.ts';
|
|
1
2
|
/** The artifact whose lock marks a run as complete enough to learn from. */
|
|
2
3
|
export declare const PHASE8_ARTIFACT = "08-memory-impact.md";
|
|
3
4
|
/** The parent's exit codes, kept as names so a caller cannot mistake one failure for the other. */
|
|
@@ -94,8 +95,16 @@ export declare function runPhase8Trigger(root: string, runId: string, options?:
|
|
|
94
95
|
*
|
|
95
96
|
* ⚠ ONE ITEM PER RUN IS NAMED, so a reader can trace a learning back to the run that produced it —
|
|
96
97
|
* and the group is never presented as more evidence than it is.
|
|
98
|
+
*
|
|
99
|
+
* ⚠ T40 — AND IT NOW CARRIES THE PLANE'S METADATA HEADER, which it did not before. `memory/domains/
|
|
100
|
+
* <subsystem>.md` is a doc the memory-plane lint validates like any other, and this renderer wrote a
|
|
101
|
+
* bare `# Learnings:` heading — so the plugin's own cross-run extraction produced a doc its own
|
|
102
|
+
* `lint_memory_plane` FAILS for nine missing fields. The extraction was right and its output shape was
|
|
103
|
+
* wrong, which is exactly the kind of defect a write surface exists to prevent.
|
|
97
104
|
*/
|
|
98
|
-
export declare function renderGroupShard(group: TrainingGroup
|
|
105
|
+
export declare function renderGroupShard(group: TrainingGroup, options?: {
|
|
106
|
+
lastValidated?: string;
|
|
107
|
+
}): string;
|
|
99
108
|
/**
|
|
100
109
|
* T30 — the extractor, resolved from the environment.
|
|
101
110
|
*
|
|
@@ -182,7 +191,10 @@ export declare function updateMemoryRegistry(existing: string, entries: Readonly
|
|
|
182
191
|
* one training shard from another here. Named so a reader can disagree with it instead of discovering it.
|
|
183
192
|
*/
|
|
184
193
|
export declare function taskTypeShardPath(mode: TrainingGroup['mode']): string;
|
|
185
|
-
|
|
194
|
+
/** T40: same metadata-header reason as {@link renderGroupShard} — see the note there. */
|
|
195
|
+
export declare function renderTaskTypeShard(groups: readonly TrainingGroup[], options?: {
|
|
196
|
+
lastValidated?: string;
|
|
197
|
+
}): string;
|
|
186
198
|
/**
|
|
187
199
|
* T30 — turn the extractor's payload into items the grouping can use.
|
|
188
200
|
*
|
|
@@ -209,3 +221,229 @@ export declare function extractAndGroup(runner: (cmd: string) => ExtractorRun, e
|
|
|
209
221
|
items: TrainingItem[];
|
|
210
222
|
groups: TrainingGroup[];
|
|
211
223
|
};
|
|
224
|
+
/**
|
|
225
|
+
* T40 — THE PLUGIN'S OWN WAY TO WRITE `.recursive/memory/`.
|
|
226
|
+
*
|
|
227
|
+
* ⚠ WHY A WRITE SURFACE AND NOT ANOTHER PARAGRAPH OF INSTRUCTIONS. The memory plane's shape is not a
|
|
228
|
+
* convention an author can guess: a durable doc must carry nine metadata fields, its `Type` must be
|
|
229
|
+
* one of five, and the plane's own lint FAILs the whole run when one is missing. Measured against the
|
|
230
|
+
* live workspace, the plane held NOTHING but the bootstrap placeholders — so the practical choice was
|
|
231
|
+
* between an agent hand-rolling a doc that fails the plane lint and no doc at all. This renders the
|
|
232
|
+
* canonical shape, stamps the provenance the phase-8 gate reads back, replaces the shard's line in
|
|
233
|
+
* the registry, and refuses to write a doc its own linter would reject.
|
|
234
|
+
*
|
|
235
|
+
* ⚠ WHAT IS DELIBERATE, DECIDED HERE RATHER THAN LEFT TO A CALLER:
|
|
236
|
+
*
|
|
237
|
+
* - ATOMIC. `writeFileSync` to a sibling temp name, then a rename over the target. A memory doc is
|
|
238
|
+
* read by the loader and linted by the plane, and a torn write is worse than a missing one: a
|
|
239
|
+
* half-written doc is a FAILED lint that names a file nobody wrote, and there is no way to tell it
|
|
240
|
+
* from a real one afterwards.
|
|
241
|
+
* - PROVENANCE-CARRYING. `Source-Runs` names the run, and it is not decoration: it is the entire
|
|
242
|
+
* discriminator between "the run wrote its memory" and "the run cited a shard that already
|
|
243
|
+
* existed", which is the distinction the phase-8 gate turns on. It is also why the run id is
|
|
244
|
+
* stamped from the CALL rather than trusted per-doc.
|
|
245
|
+
* - DEDUPLICATED, IDEMPOTENTLY. Writing the same doc again from the same run is a no-op that SAYS
|
|
246
|
+
* so (`UNCHANGED`), because phase 8 closeout runs more than once — the training trigger's own
|
|
247
|
+
* reason for existing is the re-run — and a re-run must not duplicate a lesson or rewrite a doc
|
|
248
|
+
* byte-differently for no reason.
|
|
249
|
+
* - REFUSED WHEN THE ENTRY ALREADY EXISTS, unless the caller supersedes explicitly. Another run's
|
|
250
|
+
* shard is that run's evidence; silently overwriting it destroys provenance for a shard the plane
|
|
251
|
+
* could not restore. `supersede: true` is the deliberate path, and it ARCHIVES the previous
|
|
252
|
+
* revision under `memory/archive/` first — supersede, never delete, the same discipline the
|
|
253
|
+
* counters and the registry already follow.
|
|
254
|
+
* - AND NOTHING IS INVENTED. An empty scope or body, an unknown kind, a slug that is not a name, or
|
|
255
|
+
* a doc the linter would reject all come back as typed results with `written: false` — "zero
|
|
256
|
+
* writes" is a property a test asserts by listing the tree, not a promise in a comment.
|
|
257
|
+
*
|
|
258
|
+
* ⚠ THIS WRITES THIS PLUGIN'S MEMORY AND NOTHING ELSE. It never calls, wraps or delegates to another
|
|
259
|
+
* plugin's memory tools: the plane it touches is the `.recursive/memory/` tree this plugin scaffolds
|
|
260
|
+
* and lints, reached through this repo's own paths.
|
|
261
|
+
*/
|
|
262
|
+
export interface MemoryDocSpec {
|
|
263
|
+
kind: MemoryDocKind;
|
|
264
|
+
/** The run that wrote it. Stamped into `Source-Runs`, which is what the phase-8 gate reads back. */
|
|
265
|
+
runId: string;
|
|
266
|
+
/** Filename stem (`03-ambientcss-redesign`). Sanitised: a slug is a NAME, never a path. */
|
|
267
|
+
slug: string;
|
|
268
|
+
/** Defaults to the slug. */
|
|
269
|
+
title?: string;
|
|
270
|
+
/** What the doc is about, in one sentence — the field a later run ranks on. */
|
|
271
|
+
scope: string;
|
|
272
|
+
/** The lesson itself. Empty is REFUSED rather than padded: an invented lesson is not memory. */
|
|
273
|
+
body: string;
|
|
274
|
+
status?: string;
|
|
275
|
+
ownsPaths?: readonly string[];
|
|
276
|
+
watchPaths?: readonly string[];
|
|
277
|
+
tags?: readonly string[];
|
|
278
|
+
/** The commit the doc was validated against. Defaults to a token NAMING the run (see below). */
|
|
279
|
+
validatedAtCommit?: string;
|
|
280
|
+
/** ISO timestamp. Defaults to the moment of the write, which is measured rather than invented. */
|
|
281
|
+
lastValidated?: string;
|
|
282
|
+
parent?: string;
|
|
283
|
+
/**
|
|
284
|
+
* Runs that contributed BEFORE this one, kept in `Source-Runs` when this write replaces a doc.
|
|
285
|
+
* Set by the writer on a supersede/update; a caller may set it, and the union is what keeps a
|
|
286
|
+
* replaced revision's history readable.
|
|
287
|
+
*/
|
|
288
|
+
priorRuns?: readonly string[];
|
|
289
|
+
}
|
|
290
|
+
export interface MemoryWriteOptions {
|
|
291
|
+
/**
|
|
292
|
+
* Replace a shard ANOTHER RUN owns, by archiving it under `memory/archive/` first. Default false:
|
|
293
|
+
* the write is REFUSED and the refusal names both remedies.
|
|
294
|
+
*/
|
|
295
|
+
supersede?: boolean;
|
|
296
|
+
/** The clock, injected so the caller (and a test) decides what "now" is. */
|
|
297
|
+
now?: () => Date;
|
|
298
|
+
}
|
|
299
|
+
export type MemoryWriteCode = 'WRITTEN' | 'UPDATED' | 'UNCHANGED' | 'REFUSED' | 'INVALID';
|
|
300
|
+
export interface MemoryWriteResult {
|
|
301
|
+
code: MemoryWriteCode;
|
|
302
|
+
/** Repo-relative path (`memory/episodes/<slug>.md`); EMPTY only when no path could be resolved. */
|
|
303
|
+
path: string;
|
|
304
|
+
/** True only for `WRITTEN` and `UPDATED`. */
|
|
305
|
+
written: boolean;
|
|
306
|
+
/** Where a superseded revision was archived, when that happened. */
|
|
307
|
+
archived: string | null;
|
|
308
|
+
/** Always says what happened — a silent refusal is a lost work item. */
|
|
309
|
+
reason: string;
|
|
310
|
+
}
|
|
311
|
+
/** `2026-10-10T08:39:59Z` — the lock fields' own timestamp shape, and the docs' `Last-Validated` one. */
|
|
312
|
+
export declare function isoSeconds(now?: Date): string;
|
|
313
|
+
/** A slug is a FILE NAME: no separator, no traversal, no extension, no leading dot. */
|
|
314
|
+
export declare function sanitizeMemorySlug(slug: string): string;
|
|
315
|
+
/** The repo-relative path a doc gets, or null when the kind or the slug cannot name one. */
|
|
316
|
+
export declare function memoryDocRelativePath(kind: MemoryDocKind, slug: string): string | null;
|
|
317
|
+
/**
|
|
318
|
+
* The metadata header every durable doc carries: the nine fields `lint_memory_doc` requires, in the
|
|
319
|
+
* order the SHIPPED docs use them (`Owns-Paths:` / `Watch-Paths:` / `Tags:` stand bare when empty,
|
|
320
|
+
* which is what the workspace's own promoted docs do and what `has_header_field` accepts).
|
|
321
|
+
*
|
|
322
|
+
* ⚠ A BACKTICK INSIDE A FIELD VALUE IS REPLACED, NOT ESCAPED, because these values are read back by
|
|
323
|
+
* a line-based field reader: a stray backtick would end the value early and leave the rest of the
|
|
324
|
+
* sentence in the doc as if it were a field.
|
|
325
|
+
*/
|
|
326
|
+
export declare function renderMemoryMetadata(input: {
|
|
327
|
+
type: string;
|
|
328
|
+
status: string;
|
|
329
|
+
scope: string;
|
|
330
|
+
sourceRuns: readonly string[];
|
|
331
|
+
validatedAtCommit: string;
|
|
332
|
+
lastValidated: string;
|
|
333
|
+
ownsPaths?: readonly string[];
|
|
334
|
+
watchPaths?: readonly string[];
|
|
335
|
+
tags?: readonly string[];
|
|
336
|
+
parent?: string;
|
|
337
|
+
}): string;
|
|
338
|
+
/**
|
|
339
|
+
* Render the whole doc: the canonical header, then the title, then the lesson.
|
|
340
|
+
*
|
|
341
|
+
* ⚠ `Validated-At-Commit` DEFAULTS TO A TOKEN THAT NAMES THE RUN, NOT A SHA. A run writing its own
|
|
342
|
+
* lesson has no commit to be validated against yet, and a placeholder SHA would be a fabricated fact
|
|
343
|
+
* in the one field a reader uses to decide whether the lesson still holds. The shipped generic docs
|
|
344
|
+
* use the same convention (`generic-repository-guidance`).
|
|
345
|
+
*/
|
|
346
|
+
export declare function renderMemoryDoc(spec: MemoryDocSpec, options?: {
|
|
347
|
+
lastValidated?: string;
|
|
348
|
+
}): string;
|
|
349
|
+
/**
|
|
350
|
+
* The problems that would make the memory plane FAIL this doc — checked with the LINTER'S own field
|
|
351
|
+
* list, allowed Types and allowed Statuses, so this cannot accept a doc the plane rejects.
|
|
352
|
+
*/
|
|
353
|
+
export declare function memoryDocProblems(content: string): string[];
|
|
354
|
+
/**
|
|
355
|
+
* The values of a list field (`Source-Runs:`), inline or as bullets.
|
|
356
|
+
*
|
|
357
|
+
* ⚠ THE BLOCK ENDS AT THE FIRST BLANK LINE, HEADING OR NEW FIELD, and that strictness is the point:
|
|
358
|
+
* the alternative is a parser that reads an unrelated bullet list further down the document as
|
|
359
|
+
* provenance — and provenance is the one thing here that must never be guessed.
|
|
360
|
+
*/
|
|
361
|
+
export declare function parseMemoryListField(content: string, fieldName: string): string[];
|
|
362
|
+
/** The runs a doc's `Source-Runs` names. EXACT matches: `run-1` is not `run-10`. */
|
|
363
|
+
export declare function memoryDocProvenance(content: string): string[];
|
|
364
|
+
/** The task-type token a shard's registry line carries, from the directory it lives in. */
|
|
365
|
+
export declare function taskTypeForMemoryPath(path: string): string;
|
|
366
|
+
/**
|
|
367
|
+
* Write ONE durable doc, atomically, with provenance — and refuse rather than guess.
|
|
368
|
+
*
|
|
369
|
+
* The four decided behaviours (see {@link MemoryDocSpec}'s block comment): atomic, provenance-carrying,
|
|
370
|
+
* idempotent for the same run, and refused when another run owns the path unless `supersede` is
|
|
371
|
+
* explicit — in which case the previous revision is ARCHIVED first, so nothing is ever deleted.
|
|
372
|
+
*/
|
|
373
|
+
export declare function writeMemoryDoc(root: string, spec: MemoryDocSpec, options?: MemoryWriteOptions): MemoryWriteResult;
|
|
374
|
+
export interface RunMemoryWriteResult {
|
|
375
|
+
/** Repo-relative paths actually written (`WRITTEN` or `UPDATED`), in order. */
|
|
376
|
+
writes: string[];
|
|
377
|
+
results: MemoryWriteResult[];
|
|
378
|
+
/** The registry write (`memory/MEMORY.md`), or null when nothing was written. */
|
|
379
|
+
registry: MemoryWriteResult | null;
|
|
380
|
+
/** What happened, INCLUDING the failure sentence — a caller must not have to infer it. */
|
|
381
|
+
reason: string;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* T40 — THE PHASE-8 CALL: write this run's durable docs and register them.
|
|
385
|
+
*
|
|
386
|
+
* ⚠ THE RUN ID COMES FROM THE ARGUMENT, NOT FROM EACH SPEC. A spec that named a different run would
|
|
387
|
+
* write provenance the phase-8 gate then refuses — a doc claiming a write this run did not make —
|
|
388
|
+
* and the caller would be left holding a file that blocks its own lock. One run per call removes
|
|
389
|
+
* that possibility instead of documenting it.
|
|
390
|
+
*
|
|
391
|
+
* ⚠ THE REGISTRY IS REFRESHED ONLY WHEN SOMETHING WAS WRITTEN, because the failure paths must write
|
|
392
|
+
* NOTHING (the module's standing contract) and because a registry refreshed over an unchanged plane
|
|
393
|
+
* is a claim that something changed. `updateMemoryRegistry` is reused rather than reimplemented: a
|
|
394
|
+
* shard's line is REPLACED, never duplicated, and a shard is never removed.
|
|
395
|
+
*
|
|
396
|
+
* ⚠ AND IT IS THE PLANE'S REGISTRY, `.recursive/memory/MEMORY.md` — the file `MEMORY_INDEX_FILE`
|
|
397
|
+
* names and the same one `bootstrap.ts` marker-upserts. The trigger's older `memory/MEMORY.md` is read
|
|
398
|
+
* as a FALLBACK so a registry the previous call site wrote is not silently discarded, and it is never
|
|
399
|
+
* written to: two registries in one workspace would be two answers to "what does this plane hold".
|
|
400
|
+
*/
|
|
401
|
+
export declare function writeRunMemory(root: string, runId: string, specs: readonly MemoryDocSpec[], options?: MemoryWriteOptions): RunMemoryWriteResult;
|
|
402
|
+
/** Every `.recursive/memory/**` path a text declares, in the absolute or repo-relative spelling. */
|
|
403
|
+
export declare function phase8MemoryRefs(text: string): string[];
|
|
404
|
+
export interface Phase8MemoryEvidence {
|
|
405
|
+
ok: boolean;
|
|
406
|
+
/** Paths under the plane the artifact declares, repo-relative to the repo root (sorted, deduped). */
|
|
407
|
+
declared: string[];
|
|
408
|
+
/** Declared paths that exist on disk. */
|
|
409
|
+
existing: string[];
|
|
410
|
+
/** Declared paths whose own text carries THIS run's provenance: the writes that COUNT. */
|
|
411
|
+
written: string[];
|
|
412
|
+
reason: string;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* T40 — THE CHECKABLE FACT BEHIND "the run wrote its durable memory".
|
|
416
|
+
*
|
|
417
|
+
* Three conditions, and each one exists because of a way the claim could be made without the work
|
|
418
|
+
* being done: the artifact DECLARES a path under the plane (not prose about memory in general); the
|
|
419
|
+
* path EXISTS (a declaration is not a write); and the doc on disk carries `Source-Runs` naming THIS
|
|
420
|
+
* run (a shard the run merely read, or one an earlier run wrote, is not this run's memory).
|
|
421
|
+
*
|
|
422
|
+
* ⚠ WHAT IT CANNOT TELL, stated rather than hidden: WHEN the doc was written. A run that wrote a
|
|
423
|
+
* memory doc before phase 8 and cites it here passes — and the phase 6/7 baselines deny memory-plane
|
|
424
|
+
* writes outright (`phaseBaselineRules`), so within this workflow the only phase that can produce
|
|
425
|
+
* such a doc is 8. The check is about the FACT existing at lock time, not about the clock.
|
|
426
|
+
*/
|
|
427
|
+
export declare function phase8MemoryEvidence(root: string, runId: string, artifactText: string, readText?: (path: string) => string | null): Phase8MemoryEvidence;
|
|
428
|
+
/**
|
|
429
|
+
* T40 — THE REFUSAL, as a sentence, or null when the run may lock.
|
|
430
|
+
*
|
|
431
|
+
* ⚠ THE MESSAGE NAMES THE REMEDY, not only the fault. A refusal that says "no memory doc" leaves the
|
|
432
|
+
* agent to guess a format it cannot guess (nine fields, five allowed Types, a provenance list), which
|
|
433
|
+
* is how the step became a ticked box in the first place.
|
|
434
|
+
*/
|
|
435
|
+
export declare function phase8MemoryRefusal(root: string, runId: string, artifactText: string, readText?: (path: string) => string | null): string | null;
|
|
436
|
+
/**
|
|
437
|
+
* T40 — THE LOCK-TIME ENTRY POINT: the refusal for the artifact being locked, or null.
|
|
438
|
+
*
|
|
439
|
+
* ⚠ THIS EXISTS SO THE GATE IS ONE LINE AT ITS CALL SITE. The decision (declared → exists → carries
|
|
440
|
+
* this run's provenance) belongs in this module with the write surface that produces it; `lockArtifact`
|
|
441
|
+
* should have to say only WHICH artifact it is locking, not how a memory doc is recognised. A caller
|
|
442
|
+
* that has to reproduce the rule would be a second copy of it.
|
|
443
|
+
*
|
|
444
|
+
* ⚠ AND A MISSING ARTIFACT IS NOT THIS GATE'S REFUSAL. `lockArtifact` already refuses an absent
|
|
445
|
+
* artifact before any gate can run, and returning a memory refusal for a file that does not exist
|
|
446
|
+
* would replace "the artifact is missing" with a sentence about memory — a misleading diagnosis in
|
|
447
|
+
* exchange for nothing.
|
|
448
|
+
*/
|
|
449
|
+
export declare function phase8MemoryLockRefusal(root: string, runId: string, artifact: string): string | null;
|
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.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|