@try-works/dsh-recursive-mode 0.3.0 → 0.4.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 +959 -0
- package/lib/client.js +9 -2
- package/lib/closeout-report.d.ts +113 -0
- package/lib/closeout-standards.d.ts +35 -0
- package/lib/closeout.d.ts +12 -0
- package/lib/commands.d.ts +1 -1
- package/lib/config.d.ts +202 -0
- package/lib/delegation.d.ts +123 -3
- package/lib/enforcement.d.ts +90 -1
- package/lib/errors.d.ts +168 -0
- package/lib/git-context.d.ts +17 -0
- package/lib/guard-log.d.ts +39 -0
- package/lib/handoff.d.ts +29 -0
- package/lib/hooks.d.ts +103 -0
- package/lib/identity.d.ts +61 -0
- package/lib/index.d.ts +33 -12
- package/lib/index.js +10017 -3969
- package/lib/job-log.d.ts +34 -0
- package/lib/jobs-runner.d.ts +105 -0
- package/lib/json-safe.d.ts +33 -0
- package/lib/lock.d.ts +42 -0
- package/lib/memory-feedback.d.ts +52 -0
- package/lib/memory-select.d.ts +78 -0
- package/lib/memory.d.ts +137 -0
- package/lib/model-inventory.d.ts +106 -0
- package/lib/phase-graph.d.ts +111 -0
- package/lib/phase-rules.d.ts +67 -8
- package/lib/plan-gate.d.ts +68 -0
- package/lib/policy-globs.d.ts +222 -0
- package/lib/policy-write.d.ts +42 -0
- package/lib/policy.d.ts +39 -0
- package/lib/recursive_ask.tool.d.ts +88 -0
- package/lib/recursive_closeout.tool.d.ts +1 -1
- package/lib/recursive_delegate.tool.d.ts +22 -0
- package/lib/recursive_preview.tool.d.ts +48 -0
- package/lib/recursive_review.tool.d.ts +28 -0
- package/lib/result-cap.d.ts +70 -0
- package/lib/review-round.d.ts +82 -0
- package/lib/review.d.ts +9 -0
- package/lib/role-route.d.ts +122 -0
- package/lib/router.d.ts +90 -5
- package/lib/runtime.d.ts +252 -12
- package/lib/settlement.d.ts +132 -0
- package/lib/skills-phase.d.ts +71 -0
- package/lib/skills.d.ts +70 -0
- package/lib/status.d.ts +53 -1
- package/lib/teams-loop.d.ts +91 -2
- package/lib/training.d.ts +211 -0
- package/lib/ts-lint.d.ts +15 -0
- package/lib/types.d.ts +48 -0
- package/lib/workflow-audit.d.ts +207 -0
- package/package.json +31 -31
- package/preset/recursive.patch.yml +312 -0
- package/scripts/e2e-run.mjs +51 -0
- package/scripts/link-dsh.mjs +233 -0
- package/scripts/live/fake-llm.mjs +150 -0
- package/scripts/live-session-plugin.mjs +179 -0
- package/scripts/live-session-stock.mjs +106 -0
- package/scripts/live-session.mjs +139 -0
- package/skills/recursive-mode/SKILL.md +66 -0
- package/src/client/derive.ts +18 -2
- package/src/closeout-report.ts +274 -0
- package/src/closeout-standards.ts +102 -0
- package/src/closeout.ts +39 -2
- package/src/commands.ts +116 -4
- package/src/config.ts +113 -0
- package/src/delegation.ts +336 -18
- package/src/enforcement.ts +262 -72
- package/src/errors.ts +197 -0
- package/src/git-context.ts +33 -2
- package/src/guard-log.ts +134 -0
- package/src/handoff.ts +62 -0
- package/src/hooks.ts +316 -0
- package/src/identity.ts +230 -0
- package/src/index.ts +394 -20
- package/src/job-log.ts +112 -0
- package/src/jobs-runner.ts +222 -0
- package/src/json-safe.ts +75 -0
- package/src/lock.ts +153 -16
- package/src/memory-feedback.ts +185 -0
- package/src/memory-select.ts +187 -0
- package/src/memory.ts +309 -0
- package/src/model-inventory.ts +196 -0
- package/src/phase-graph.ts +191 -0
- package/src/phase-rules.ts +236 -0
- package/src/plan-gate.ts +111 -0
- package/src/policy-globs.ts +636 -0
- package/src/policy-write.ts +210 -0
- package/src/policy.ts +70 -5
- package/src/recursive_ask.tool.ts +276 -0
- package/src/recursive_audit_team.tool.ts +7 -3
- package/src/recursive_closeout.tool.ts +36 -35
- package/src/recursive_delegate.tool.ts +194 -0
- package/src/recursive_init.tool.ts +4 -3
- package/src/recursive_lint.tool.ts +81 -6
- package/src/recursive_lock.tool.ts +21 -4
- package/src/recursive_phase.tool.ts +3 -2
- package/src/recursive_preview.tool.ts +142 -0
- package/src/recursive_review.tool.ts +190 -0
- package/src/recursive_scratch.tool.ts +5 -4
- package/src/recursive_status.tool.ts +3 -2
- package/src/recursive_worktree.tool.ts +6 -5
- package/src/result-cap.ts +130 -0
- package/src/review-round.ts +335 -0
- package/src/review.ts +17 -3
- package/src/role-route.ts +230 -0
- package/src/router.ts +128 -2
- package/src/runtime.ts +968 -39
- package/src/settlement.ts +355 -0
- package/src/skills-phase.ts +143 -0
- package/src/skills.ts +151 -0
- package/src/snapshot.ts +39 -8
- package/src/status.ts +209 -4
- package/src/teams-loop.ts +223 -9
- package/src/training.ts +565 -0
- package/src/ts-lint.ts +38 -4
- package/src/types.ts +51 -0
- package/src/workflow-audit.ts +288 -0
- package/scripts/install-preset.cmd +0 -7
- package/scripts/install-preset.js +0 -101
package/lib/status.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import type { ArtifactState, PhaseDef, RecursiveStatusResult } from './types.ts';
|
|
1
|
+
import type { ArtifactState, PendingWorkItem, PhaseDef, PhasePosition, RecursiveStatusResult } from './types.ts';
|
|
2
|
+
import { PHASE_POSITIONS } from './types.ts';
|
|
3
|
+
export { PHASE_POSITIONS };
|
|
4
|
+
export type { ArtifactState, PhasePosition };
|
|
5
|
+
/**
|
|
6
|
+
* T21 — derive the ONE named position of a phase from the fields consumers
|
|
7
|
+
* currently recombine for themselves. Total and disjoint: every reachable shape
|
|
8
|
+
* returns exactly one member of {@link PHASE_POSITIONS}, so two consumers cannot
|
|
9
|
+
* read the same phase and disagree.
|
|
10
|
+
*
|
|
11
|
+
* Order is the whole design. `tampered` outranks every other lock problem
|
|
12
|
+
* because a content/hash divergence is the more serious fact — a locked artifact
|
|
13
|
+
* whose bytes changed invalidates everything that cited it, whereas a failed gate
|
|
14
|
+
* on an otherwise-intact artifact is a fixable omission.
|
|
15
|
+
*/
|
|
16
|
+
export declare function phasePosition(state: ArtifactState): PhasePosition;
|
|
2
17
|
export declare const RUN_ARTIFACT_SEQUENCE: string[];
|
|
3
18
|
export declare const PHASES: PhaseDef[];
|
|
4
19
|
export declare function escapeRegExp(value: string): string;
|
|
@@ -15,5 +30,42 @@ export declare function lockHashFromContent(content: string): string;
|
|
|
15
30
|
export declare function getWorkflowProfile(runDir: string): string;
|
|
16
31
|
export { getLatestRunDirectory, discoverRuns, resolveRunDir } from './run.ts';
|
|
17
32
|
export type { RunDiscoveryResult } from './run.ts';
|
|
33
|
+
/**
|
|
34
|
+
* T18 — unresolved in-flight work, DERIVED from the run directory.
|
|
35
|
+
*
|
|
36
|
+
* There is no ledger, no queue and no stored flag: this reads what is already on
|
|
37
|
+
* disk, which is what makes the quiescence rule in `lockArtifact` cheap enough to
|
|
38
|
+
* run on every lock. Plan §4.0: derived beats stored wherever derivation is
|
|
39
|
+
* cheap, and losing a derived fact costs nothing because it cannot be lost.
|
|
40
|
+
*
|
|
41
|
+
* WHY THIS IS THE ONLY CASE IMPLEMENTED. The pairing is created early in the
|
|
42
|
+
* happy path — `runtime.ts` writes `subagents/<id>/handoff.md` BEFORE the bundle
|
|
43
|
+
* and before the child starts — so the exposure is exactly the window between
|
|
44
|
+
* the handoff and the reply. The plan also names "a reopen plan without a
|
|
45
|
+
* completion marker" and "a closeout phase scaffolded without a receipt"; neither
|
|
46
|
+
* exists on disk (`reopenArtifact` reverts the artifact in place and invalidates
|
|
47
|
+
* receipts; a scaffolded-but-unlocked closeout artifact is the normal pre-lock
|
|
48
|
+
* state of every phase), and enforcing either would make locking impossible.
|
|
49
|
+
* That omission is deliberate and recorded on the item.
|
|
50
|
+
*
|
|
51
|
+
* A reply that exists but is empty is NOT a submission, so it stays pending —
|
|
52
|
+
* a child that created the file and wrote nothing has not answered.
|
|
53
|
+
*
|
|
54
|
+
* Total: a missing or unreadable run directory yields an empty set, never a throw.
|
|
55
|
+
*/
|
|
56
|
+
export declare function pendingWork(runDir: string): PendingWorkItem[];
|
|
18
57
|
export declare function getArtifactState(artifactPath: string, workflowProfile: string): ArtifactState;
|
|
58
|
+
/**
|
|
59
|
+
* Fold-cache diagnostics. Read-only observation, kept in the shipped API because
|
|
60
|
+
* "is the frame hitting?" is the first question when the board feels slow, and a
|
|
61
|
+
* counter is cheaper than guessing. `reparsed` counts EXISTING artifacts re-read
|
|
62
|
+
* (an absent one costs only an existsSync).
|
|
63
|
+
*/
|
|
64
|
+
export declare function foldDiagnostics(): {
|
|
65
|
+
folds: number;
|
|
66
|
+
reuses: number;
|
|
67
|
+
reparsed: number;
|
|
68
|
+
};
|
|
69
|
+
/** Drop every frame and counter. For tests, and for a caller that knows the tree changed underneath. */
|
|
70
|
+
export declare function resetFoldCache(): void;
|
|
19
71
|
export declare function foldRun(runDir: string, runId: string): RecursiveStatusResult;
|
package/lib/teams-loop.d.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The recursion concept's core state machine, expressed over the native
|
|
5
5
|
* `agentTeams` service: ONE durable task per phase carries the loop —
|
|
6
|
-
* createTask (pending) → claim (in_progress) → audit round → on REVISE
|
|
7
|
-
* updateTask(edit, repair instruction) → re-audit the SAME task → on APPROVE
|
|
6
|
+
* createTask (pending) → claim (in_progress) → audit round → on REVISE* updateTask(edit, repair instruction) → re-audit the SAME task → on APPROVE
|
|
8
7
|
* updateTask(complete) → lock. A stuck reviewer is interrupted through the
|
|
9
8
|
* team's own kill switch; a REJECT or round-cap releases the task and fails
|
|
10
9
|
* loud — a lock NEVER happens before an APPROVE verdict.
|
|
@@ -104,6 +103,11 @@ export interface AuditLoopRound {
|
|
|
104
103
|
readonly verdict: AuditVerdict;
|
|
105
104
|
readonly repair?: string;
|
|
106
105
|
readonly taskRevision: number;
|
|
106
|
+
/**
|
|
107
|
+
* T20: true when this round produced the SAME progress cursor as its predecessor —
|
|
108
|
+
* the same finding, restated. Rendered so the board shows a loop as a loop.
|
|
109
|
+
*/
|
|
110
|
+
readonly noProgress?: boolean;
|
|
107
111
|
}
|
|
108
112
|
/** The auditToPass result. */
|
|
109
113
|
export interface AuditToPassResult {
|
|
@@ -116,6 +120,52 @@ export interface AuditToPassResult {
|
|
|
116
120
|
readonly locked: boolean;
|
|
117
121
|
/** Latest task view (board-facing per-phase history). */
|
|
118
122
|
readonly taskView?: TeamTaskViewLike;
|
|
123
|
+
/**
|
|
124
|
+
* T19: true when this call was recognised as a repeat of an audit-to-pass that had
|
|
125
|
+
* already completed for this run and phase, so no second task was created. The
|
|
126
|
+
* caller must read the PHASE's lock state itself — this result cannot claim the
|
|
127
|
+
* artifact is locked, because a recognised repeat performs no locking.
|
|
128
|
+
*/
|
|
129
|
+
readonly recognisedRepeat?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* T20: how many rounds ran. The absolute backstop, reported so a caller can see how
|
|
132
|
+
* close the loop came to its cap.
|
|
133
|
+
*/
|
|
134
|
+
readonly attempts?: number;
|
|
135
|
+
/** T20: consecutive rounds that produced the SAME progress cursor. */
|
|
136
|
+
readonly consecutiveNoProgress?: number;
|
|
137
|
+
/** T20: the last round's progress cursor — what the round said was wrong. */
|
|
138
|
+
readonly progressCursor?: string;
|
|
139
|
+
/** T28: how many repairs were requested before the loop stopped. */
|
|
140
|
+
readonly repairAttempts?: number;
|
|
141
|
+
/**
|
|
142
|
+
* T20: true when the loop stopped in a state that must NOT be retried
|
|
143
|
+
* automatically — no progress, or the attempt cap. The caller has to decide to
|
|
144
|
+
* resume, deliberately, rather than a loop quietly grinding on or quietly giving up.
|
|
145
|
+
*/
|
|
146
|
+
readonly resumeRequired?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* T19: the operation-index seam.
|
|
150
|
+
*
|
|
151
|
+
* INJECTED, not imported. `auditToPass` is pure over its seams — it has no `runDir`
|
|
152
|
+
* and no filesystem access, which is exactly what lets the whole loop be driven by
|
|
153
|
+
* fakes in tests. Reaching for `recordOperation` directly would have traded that away
|
|
154
|
+
* for one line of convenience, so the caller supplies persistence instead.
|
|
155
|
+
*/
|
|
156
|
+
export interface AuditOperationsSeamLike {
|
|
157
|
+
/** The previously recorded attempt for an id, or null when it is new. */
|
|
158
|
+
find?: (id: string) => {
|
|
159
|
+
id: string;
|
|
160
|
+
outcome?: string;
|
|
161
|
+
} | null;
|
|
162
|
+
/** Record one attempt. Best-effort: a failed write must not change the loop's outcome. */
|
|
163
|
+
record?: (record: {
|
|
164
|
+
id: string;
|
|
165
|
+
act: string;
|
|
166
|
+
at: string;
|
|
167
|
+
outcome?: string;
|
|
168
|
+
}) => void;
|
|
119
169
|
}
|
|
120
170
|
/** Inputs for one audit-to-pass loop. */
|
|
121
171
|
export interface AuditToPassInput {
|
|
@@ -130,6 +180,11 @@ export interface AuditToPassInput {
|
|
|
130
180
|
readonly blockedBy?: readonly TeamTaskIdLike[];
|
|
131
181
|
/** Write scopes for the phase artifact (advisory, overlap-warned). */
|
|
132
182
|
readonly writeScopes?: readonly string[];
|
|
183
|
+
/**
|
|
184
|
+
* T19: the operation index, injected. Absent it, the loop runs exactly as before —
|
|
185
|
+
* no pre-check, no records, no behaviour change.
|
|
186
|
+
*/
|
|
187
|
+
readonly operations?: AuditOperationsSeamLike;
|
|
133
188
|
/** Run ONE audit round for the current task; live usage delegates (T4). */
|
|
134
189
|
readonly runAuditRound: (round: number, task: TeamTaskViewLike) => Promise<AuditRoundOutcome>;
|
|
135
190
|
/** Lock the phase artifact — called ONLY after an APPROVE verdict. */
|
|
@@ -138,6 +193,25 @@ export interface AuditToPassInput {
|
|
|
138
193
|
readonly reviewerName?: string;
|
|
139
194
|
/** Round cap (fail loud past it; no lock). */
|
|
140
195
|
readonly maxRounds?: number;
|
|
196
|
+
/**
|
|
197
|
+
* T20: how many CONSECUTIVE rounds may repeat the same progress cursor before the
|
|
198
|
+
* loop stops and demands a resume. Default 2, so three identical rounds terminate:
|
|
199
|
+
* the first sets the cursor and the next two repeat it. Absent the bound, a phase
|
|
200
|
+
* could restate one unfixed finding until the attempt cap, which is the wrong
|
|
201
|
+
* budget — a round count indulges a loop while cutting off genuine progress.
|
|
202
|
+
*/
|
|
203
|
+
readonly maxNoProgress?: number;
|
|
204
|
+
/**
|
|
205
|
+
* T28: the configurable caps. `maxAuditRounds` is the absolute round ceiling and
|
|
206
|
+
* `maxRepairAttempts` bounds how many times a phase may be sent back for repair
|
|
207
|
+
* even when every round finds something NEW — the case T20's no-progress bound
|
|
208
|
+
* deliberately lets run. Both are optional so an existing caller keeps its
|
|
209
|
+
* behaviour, with the round cap falling back to `maxRounds ?? 3`.
|
|
210
|
+
*/
|
|
211
|
+
readonly budgets?: {
|
|
212
|
+
readonly maxAuditRounds?: number;
|
|
213
|
+
readonly maxRepairAttempts?: number;
|
|
214
|
+
};
|
|
141
215
|
/** Per-round wait timeout before the audit round runs (skipped without the seam). */
|
|
142
216
|
readonly waitTimeoutMs?: number;
|
|
143
217
|
}
|
|
@@ -155,6 +229,21 @@ export declare function renderTaskHistory(task: TeamTaskViewLike | undefined, ro
|
|
|
155
229
|
* → APPROVE: updateTask(complete) → lockPhase()
|
|
156
230
|
* → REJECT / cap / stuck: updateTask(release) + interrupt, NO lock.
|
|
157
231
|
*/
|
|
232
|
+
/**
|
|
233
|
+
* T19 wrapper: recognise a repeat, then record what happened.
|
|
234
|
+
*
|
|
235
|
+
* The identity covers this run and phase. A call whose operation was already recorded
|
|
236
|
+
* as APPLIED returns immediately without creating a second task — the acceptance's
|
|
237
|
+
* "recognised no-op rather than a second execution". Every other outcome (rejected,
|
|
238
|
+
* capped, errored) records as `unaccepted`, so a retry still runs: a failed audit must
|
|
239
|
+
* stay retryable, and only a COMPLETED one is a repeat.
|
|
240
|
+
*
|
|
241
|
+
* Per-round entries are recorded after the loop from the result's own round trail.
|
|
242
|
+
* That is one write per call instead of one per round, and it keeps the loop body
|
|
243
|
+
* free of persistence — but it does mean a crash MID-loop leaves the whole operation
|
|
244
|
+
* unrecorded, which is the honest trade for not threading a writer through every
|
|
245
|
+
* branch.
|
|
246
|
+
*/
|
|
158
247
|
export declare function auditToPass(input: AuditToPassInput): Promise<AuditToPassResult>;
|
|
159
248
|
/** Whether a task view is currently claimed by the named owner (board-facing). */
|
|
160
249
|
export declare function isTaskClaimedBy(task: TeamTaskViewLike, ownerName: string): boolean;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** The artifact whose lock marks a run as complete enough to learn from. */
|
|
2
|
+
export declare const PHASE8_ARTIFACT = "08-memory-impact.md";
|
|
3
|
+
/** The parent's exit codes, kept as names so a caller cannot mistake one failure for the other. */
|
|
4
|
+
export declare const TRAINING_EXIT: {
|
|
5
|
+
/** The extractor could not be reached or run. */
|
|
6
|
+
readonly EXTRACTOR_UNAVAILABLE: 2;
|
|
7
|
+
/** There was not enough evidence to extract anything. */
|
|
8
|
+
readonly INSUFFICIENT_EVIDENCE: 3;
|
|
9
|
+
};
|
|
10
|
+
export type TrainingCode = 'OK' | 'EXTRACTOR_UNAVAILABLE' | 'INSUFFICIENT_EVIDENCE';
|
|
11
|
+
export interface TrainingResult {
|
|
12
|
+
code: TrainingCode;
|
|
13
|
+
/** The parent's exit code: 0 on success, otherwise 2 or 3. Never a silent success. */
|
|
14
|
+
exit: number;
|
|
15
|
+
reason: string;
|
|
16
|
+
/** Files written. EMPTY on every failure path — asserted, not promised. */
|
|
17
|
+
writes: string[];
|
|
18
|
+
}
|
|
19
|
+
/** How many runs have a LOCKED phase-8 artifact. The gate's only input. */
|
|
20
|
+
export declare function countPhase8LockedRuns(root: string, readText?: (path: string) => string | null): number;
|
|
21
|
+
/**
|
|
22
|
+
* The gate: extraction needs MORE THAN ONE locked run.
|
|
23
|
+
*
|
|
24
|
+
* ⚠ ONE RUN IS NOT EVIDENCE — it is an anecdote, and a memory file written from a single run would
|
|
25
|
+
* teach the next run that one run's accidents are rules. The parent skips with an explanation rather
|
|
26
|
+
* than extracting from what it has, and this returns the explanation as part of the result.
|
|
27
|
+
*/
|
|
28
|
+
export declare function trainingGate(lockedRuns: number): TrainingResult;
|
|
29
|
+
/** A learning candidate, before grouping. */
|
|
30
|
+
export interface TrainingItem {
|
|
31
|
+
runId: string;
|
|
32
|
+
/** Changed paths or cited files — the stronger signal of which subsystem this belongs to. */
|
|
33
|
+
paths: string[];
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Infer the subsystem from changed paths.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ PATHS BEAT PROSE. The parent is explicit that changed paths are the stronger signal, so this
|
|
40
|
+
* prefers them and falls back to a stable `unclassified` bucket rather than guessing from wording —
|
|
41
|
+
* a wrong subsystem files a learning where nobody will look for it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function inferSubsystem(item: TrainingItem): string;
|
|
44
|
+
export interface TrainingGroup {
|
|
45
|
+
subsystem: string;
|
|
46
|
+
items: TrainingItem[];
|
|
47
|
+
/** How many DISTINCT runs contributed. A single-run group must not train on its own. */
|
|
48
|
+
runs: number;
|
|
49
|
+
mode: 'contrastive' | 'winner-only';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Group items by subsystem and decide each group's mode.
|
|
53
|
+
*
|
|
54
|
+
* ⚠ REFUSE TO TRAIN ON A SINGLE-RUN GROUP ALONE. Two items from one run are one observation written
|
|
55
|
+
* twice, so such a group is DROPPED rather than trained on — the parent's rule, and the reason a
|
|
56
|
+
* group reports its distinct-run count. Winners and losers in one group support a CONTRASTIVE
|
|
57
|
+
* learning; otherwise the group is winner-only.
|
|
58
|
+
*/
|
|
59
|
+
export declare function groupLearnings(items: readonly TrainingItem[], isWinner: (item: TrainingItem) => boolean): TrainingGroup[];
|
|
60
|
+
/**
|
|
61
|
+
* The trigger, run at the **RE-RUN** of closeout phase 08.
|
|
62
|
+
*
|
|
63
|
+
* ⚠ NOT AT THE FIRST LOCK, deliberately and per the parent: a run that has just locked would be
|
|
64
|
+
* training on itself, and its own conclusions would be promoted to memory before anything else had a
|
|
65
|
+
* chance to contradict them. The caller says `rerun: true` to mean "phase 08 has been through closeout
|
|
66
|
+
* more than once"; the first lock reports `OK` with an empty `writes` and a reason that says why.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runPhase8Trigger(root: string, runId: string, options?: {
|
|
69
|
+
rerun?: boolean;
|
|
70
|
+
extractorAvailable?: boolean;
|
|
71
|
+
items?: readonly TrainingItem[];
|
|
72
|
+
isWinner?: (item: TrainingItem) => boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The write seam. ABSENT IS NOT SUCCESS: with no writer the groups are planned and reported, and
|
|
75
|
+
* the reason SAYS the writes did not happen — because a result that looked successful while
|
|
76
|
+
* writing nothing is precisely the failure the parent's contract forbids ("do not claim memory
|
|
77
|
+
* updates").
|
|
78
|
+
*/
|
|
79
|
+
write?: (relativePath: string, content: string) => string;
|
|
80
|
+
/**
|
|
81
|
+
* The registry read seam. WITHOUT IT THE REGISTRY IS NOT REFRESHED and the result SAYS SO, because
|
|
82
|
+
* a silent half-write would leave `MEMORY.md` describing a plane that has changed underneath it.
|
|
83
|
+
*/
|
|
84
|
+
readText?: (relativePath: string) => string | null;
|
|
85
|
+
/**
|
|
86
|
+
* FU-5: THE PRODUCTION SPAWN, injected. When the caller supplies no `items`, the trigger RUNS the
|
|
87
|
+
* extractor through this runner — which is the link that was missing entirely: `extractAndGroup` was
|
|
88
|
+
* referenced only by its own definition, so the round trip existed and nothing invoked it.
|
|
89
|
+
*/
|
|
90
|
+
runner?: (cmd: string) => ExtractorRun;
|
|
91
|
+
}): TrainingResult;
|
|
92
|
+
/**
|
|
93
|
+
* Render a group's shard.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ ONE ITEM PER RUN IS NAMED, so a reader can trace a learning back to the run that produced it —
|
|
96
|
+
* and the group is never presented as more evidence than it is.
|
|
97
|
+
*/
|
|
98
|
+
export declare function renderGroupShard(group: TrainingGroup): string;
|
|
99
|
+
/**
|
|
100
|
+
* T30 — the extractor, resolved from the environment.
|
|
101
|
+
*
|
|
102
|
+
* ⚠ THE EXTRACTOR IS NEVER EMBEDDED. The parent delegates through a command
|
|
103
|
+
* (`RECURSIVE_TRAINING_EXTRACTOR_CMD`) or a response file rather than shipping an LLM client, and this
|
|
104
|
+
* keeps that: the plugin resolves a command and hands it to a runner it is given.
|
|
105
|
+
*
|
|
106
|
+
* ⚠ WHY THE RUNNER IS INJECTED RATHER THAN SPAWNED HERE. This harness's file sandbox denies a child
|
|
107
|
+
* process the PIPED stdio a capture needs, so a module that spawned directly would be untestable in the
|
|
108
|
+
* environment it runs in — and a rule that cannot be tested is a rule that will rot. The DECISION lives
|
|
109
|
+
* here and is asserted with a fake runner; the SPAWN lives at the caller.
|
|
110
|
+
*/
|
|
111
|
+
export declare const TRAINING_EXTRACTOR_ENV = "RECURSIVE_TRAINING_EXTRACTOR_CMD";
|
|
112
|
+
/**
|
|
113
|
+
* FU-5 — the RESPONSE FILE, which is what makes the spawn possible in a confined sandbox.
|
|
114
|
+
*
|
|
115
|
+
* ⚠ WHY A FILE AND NOT A PIPE. This harness's sandbox denies a child process the piped stdio a capture
|
|
116
|
+
* needs, so a spawn that read the extractor's stdout would fail with EPERM **in the environment it runs
|
|
117
|
+
* in**. The parent's own interface already solves this: it delegates through `--response-file`, i.e. the
|
|
118
|
+
* extractor WRITES ITS ANSWER TO A PATH. The plugin spawns with `stdio: 'ignore'` (which the sandbox
|
|
119
|
+
* allows), hands the path over in an environment variable, and reads the file afterwards.
|
|
120
|
+
*/
|
|
121
|
+
export declare const TRAINING_RESPONSE_FILE_ENV = "RECURSIVE_TRAINING_RESPONSE_FILE";
|
|
122
|
+
/**
|
|
123
|
+
* Build the production runner: spawn the command, then read the file it was asked to write.
|
|
124
|
+
*
|
|
125
|
+
* ⚠ IT NEVER THROWS. A missing binary, a non-zero exit and a missing response file all come back as a
|
|
126
|
+
* non-zero `status` with an explanatory `stdout`, so `runExtractor` maps them to a TYPED failure — the
|
|
127
|
+
* closeout must not die because an extractor is misconfigured.
|
|
128
|
+
*/
|
|
129
|
+
export declare function spawnExtractorRunner(options: {
|
|
130
|
+
cwd: string;
|
|
131
|
+
responseFile: string;
|
|
132
|
+
/** Injected for tests; defaults to `node:child_process.spawnSync`. */
|
|
133
|
+
spawn?: (cmd: string, args: string[], opts: Record<string, unknown>) => {
|
|
134
|
+
status: number | null;
|
|
135
|
+
error?: Error;
|
|
136
|
+
};
|
|
137
|
+
}): (cmd: string) => ExtractorRun;
|
|
138
|
+
export declare function resolveExtractor(env: Record<string, string | undefined>): string | null;
|
|
139
|
+
/** What a runner reports back. `status` is the process exit code; `stdout` its captured output. */
|
|
140
|
+
export interface ExtractorRun {
|
|
141
|
+
status: number;
|
|
142
|
+
stdout: string;
|
|
143
|
+
}
|
|
144
|
+
export interface ExtractorOutcome {
|
|
145
|
+
ok: boolean;
|
|
146
|
+
/** Present only when `ok`; the raw JSON the extractor produced. */
|
|
147
|
+
payload?: unknown;
|
|
148
|
+
/** Present only when not `ok` — why, in the parent's terms. */
|
|
149
|
+
failure?: 'EXTRACTOR_UNAVAILABLE' | 'MALFORMED_OUTPUT';
|
|
150
|
+
reason: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Run the extractor and interpret its answer.
|
|
154
|
+
*
|
|
155
|
+
* ⚠ A NON-ZERO STATUS AND MALFORMED OUTPUT ARE BOTH FAILURES, and neither is "no items" — a malformed
|
|
156
|
+
* answer is a broken extractor, not a run with nothing to learn, and collapsing the two would report
|
|
157
|
+
* exit 3 (insufficient evidence) for a bug that deserves exit 2.
|
|
158
|
+
*/
|
|
159
|
+
export declare function runExtractor(runner: (cmd: string) => ExtractorRun, cmd: string | null): ExtractorOutcome;
|
|
160
|
+
/**
|
|
161
|
+
* T30 — the registry line for a shard, and the registry update.
|
|
162
|
+
*
|
|
163
|
+
* ⚠ THE REGISTRY IS REFRESHED BY REPLACING A SHARD'S LINE, NOT BY APPENDING. Two lines for one shard
|
|
164
|
+
* would make `MEMORY.md` claim the plane holds something twice, and the loader reads the registry
|
|
165
|
+
* first — so a duplicated marker is not cosmetic, it is a wrong answer about what exists.
|
|
166
|
+
*
|
|
167
|
+
* ⚠ AND A SHARD IS NEVER REMOVED HERE. Per the memory-worker discipline this borrows: **supersede,
|
|
168
|
+
* never delete.** A shard that stops being written keeps its line and its history; removing it is a
|
|
169
|
+
* tombstone decision, not a side effect of training.
|
|
170
|
+
*/
|
|
171
|
+
export declare function registryLine(shardPath: string, taskType: string): string;
|
|
172
|
+
export declare function updateMemoryRegistry(existing: string, entries: ReadonlyArray<{
|
|
173
|
+
path: string;
|
|
174
|
+
taskType: string;
|
|
175
|
+
}>): string;
|
|
176
|
+
/**
|
|
177
|
+
* T30 — the task-type shard.
|
|
178
|
+
*
|
|
179
|
+
* ⚠ `task-type` IS READ FROM THE GROUP'S MODE, and that is an INTERPRETATION rather than a measured
|
|
180
|
+
* fact: the parent writes `memory/training/<task-type>.md` without defining the key in the material I
|
|
181
|
+
* have, so the mode a group was extracted under (`contrastive` / `winner-only`) is what distinguishes
|
|
182
|
+
* one training shard from another here. Named so a reader can disagree with it instead of discovering it.
|
|
183
|
+
*/
|
|
184
|
+
export declare function taskTypeShardPath(mode: TrainingGroup['mode']): string;
|
|
185
|
+
export declare function renderTaskTypeShard(groups: readonly TrainingGroup[]): string;
|
|
186
|
+
/**
|
|
187
|
+
* T30 — turn the extractor's payload into items the grouping can use.
|
|
188
|
+
*
|
|
189
|
+
* ⚠ A MALFORMED PAYLOAD IS NOT A BROKEN EXTRACTOR. By the time this runs the extractor has exited 0 and
|
|
190
|
+
* produced parseable JSON, so the transport is fine; a payload carrying no items is an extractor that
|
|
191
|
+
* found nothing, which is **exit 3, not exit 2**. Keeping those apart is why `runExtractor` answers
|
|
192
|
+
* first and this second.
|
|
193
|
+
*
|
|
194
|
+
* ⚠ A PARTIAL ANSWER NEITHER LOSES THE BATCH NOR INVENTS EVIDENCE: an entry missing its text is
|
|
195
|
+
* SKIPPED rather than defaulted, because an item with invented text would be taught as a learning
|
|
196
|
+
* nobody extracted. An entry with no `runId` is skipped too — the grouping counts DISTINCT runs, so an
|
|
197
|
+
* unattributed observation would be pooled into a run it did not come from.
|
|
198
|
+
*/
|
|
199
|
+
export declare function parseExtractorItems(payload: unknown): TrainingItem[];
|
|
200
|
+
/**
|
|
201
|
+
* T30 — the whole round trip, in the order the failure codes demand: resolve the command, run it,
|
|
202
|
+
* parse the payload, then group. Each stage keeps its own meaning — no command or a non-zero exit is
|
|
203
|
+
* **exit 2**; a payload with nothing usable in it is **exit 3**.
|
|
204
|
+
*/
|
|
205
|
+
export declare function extractAndGroup(runner: (cmd: string) => ExtractorRun, env: Record<string, string | undefined>, options?: {
|
|
206
|
+
isWinner?: (item: TrainingItem) => boolean;
|
|
207
|
+
}): {
|
|
208
|
+
outcome: ExtractorOutcome;
|
|
209
|
+
items: TrainingItem[];
|
|
210
|
+
groups: TrainingGroup[];
|
|
211
|
+
};
|
package/lib/ts-lint.d.ts
CHANGED
|
@@ -123,6 +123,21 @@ export declare function getPhaseOwnedActualChangedFiles(fileName: string, actual
|
|
|
123
123
|
export declare function getRelatedAddendaPaths(runDir: string, artifactName: string): string[];
|
|
124
124
|
/** get_stage_local_addenda_paths: run_dir/addenda/<base>.addendum-*.md. */
|
|
125
125
|
export declare function getStageLocalAddendaPaths(runDir: string, artifactName: string): string[];
|
|
126
|
+
/**
|
|
127
|
+
* get_run_tree_addenda: EVERY addendum in the run, wherever it was filed.
|
|
128
|
+
*
|
|
129
|
+
* ⚠ WHY THIS EXISTS BESIDE THE TWO ABOVE: both scan `run_dir/addenda/` and nothing else, and flatly. But an
|
|
130
|
+
* addendum is written BESIDE THE ARTIFACT IT CLOSES — the run ROOT is where the real ones live, e.g.
|
|
131
|
+
* `02-to-be-plan.addendum-r4-r2-mount-resolution.md`, which the comments elsewhere in this file cite by name
|
|
132
|
+
* — and a stage may file one in a subfolder. A reader that consults only `addenda/` therefore misses
|
|
133
|
+
* exactly the addenda that closed an upstream gap, which are the ones a closeout most needs to see.
|
|
134
|
+
*
|
|
135
|
+
* Returns run-relative POSIX paths, sorted, so a caller can join them onto the run directory and cite them
|
|
136
|
+
* verbatim. Matching uses {@link isAddendumArtifact} — the SAME predicate the rest of the plugin uses — so a
|
|
137
|
+
* name can never count as an addendum in one place and not another. `upstream-gap` names are matched too:
|
|
138
|
+
* they are the back-edge form of an addendum, and {@link getRelatedAddendaPaths} already treats them as one.
|
|
139
|
+
*/
|
|
140
|
+
export declare function getRunTreeAddenda(runDir: string): string[];
|
|
126
141
|
/** get_current_phase_upstream_gap_addenda_paths: run_dir/addenda/<base>.upstream-gap.*.addendum-*.md. */
|
|
127
142
|
export declare function getCurrentPhaseUpstreamGapAddendaPaths(runDir: string, artifactName: string): string[];
|
|
128
143
|
/** get_header_field_block: lines under `Field:` until a stop field. */
|
package/lib/types.d.ts
CHANGED
|
@@ -20,6 +20,25 @@ export interface ArtifactState {
|
|
|
20
20
|
todoHasSection: boolean;
|
|
21
21
|
todoUnchecked: number;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* T21 — ONE named position per phase, derived from the fields every consumer
|
|
25
|
+
* currently recombines for itself (`status`, `lockValid`, `lockProblems`,
|
|
26
|
+
* `blockers`). Two consumers recombining those independently is exactly how two
|
|
27
|
+
* consumers come to disagree about a phase's state; a single derived value gives
|
|
28
|
+
* them nothing to disagree about.
|
|
29
|
+
*
|
|
30
|
+
* Closed vocabulary, total and disjoint (see `phasePosition`):
|
|
31
|
+
* `absent` the artifact does not exist
|
|
32
|
+
* `skipped` an optional (or legacy-profile late) phase that is not present
|
|
33
|
+
* `draft` present and unlocked, with no blockers
|
|
34
|
+
* `blocked` present and unlocked, with at least one blocker
|
|
35
|
+
* `invalid-lock` LOCKED but failing a condition other than its hash
|
|
36
|
+
* `locked` LOCKED and lock-valid
|
|
37
|
+
* `tampered` LOCKED with a hash mismatch — the more serious fact, so it wins
|
|
38
|
+
* over any other lock problem
|
|
39
|
+
*/
|
|
40
|
+
export type PhasePosition = 'absent' | 'skipped' | 'draft' | 'blocked' | 'invalid-lock' | 'locked' | 'tampered';
|
|
41
|
+
export declare const PHASE_POSITIONS: readonly PhasePosition[];
|
|
23
42
|
export interface PhaseState {
|
|
24
43
|
key: string;
|
|
25
44
|
label: string;
|
|
@@ -30,6 +49,8 @@ export interface PhaseState {
|
|
|
30
49
|
lockValid: boolean;
|
|
31
50
|
lockProblems: string[];
|
|
32
51
|
blockers: string[];
|
|
52
|
+
/** T21: the single derived state of this phase. */
|
|
53
|
+
position: PhasePosition;
|
|
33
54
|
}
|
|
34
55
|
export interface RecursiveStatusResult {
|
|
35
56
|
runId: string;
|
|
@@ -59,6 +80,21 @@ export interface RecursiveTamper {
|
|
|
59
80
|
path: string;
|
|
60
81
|
reason: string;
|
|
61
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* T18 — one piece of unresolved in-flight work, DERIVED from the run directory
|
|
85
|
+
* (there is no ledger). `unanswered-delegation` is a `handoff.md` with no reply
|
|
86
|
+
* yet; `empty-reply` is a reply file that exists but carries nothing, which is
|
|
87
|
+
* not a submission.
|
|
88
|
+
*/
|
|
89
|
+
export interface PendingWorkItem {
|
|
90
|
+
kind: 'unanswered-delegation' | 'empty-reply';
|
|
91
|
+
/** The delegation directory name, so a refusal can name what is blocking. */
|
|
92
|
+
delegationId: string;
|
|
93
|
+
/** Repo-relative path of the file that would settle it, or of the handoff. */
|
|
94
|
+
path: string;
|
|
95
|
+
/** One sentence for a human, naming the delegation and what is missing. */
|
|
96
|
+
detail: string;
|
|
97
|
+
}
|
|
62
98
|
/** One subagent activity fact (start or end). */
|
|
63
99
|
export interface RecursiveSubagent {
|
|
64
100
|
childId: string;
|
|
@@ -72,6 +108,12 @@ export interface RecursivePhaseRow {
|
|
|
72
108
|
status: string;
|
|
73
109
|
lockedAt?: string;
|
|
74
110
|
lockHash?: string;
|
|
111
|
+
/**
|
|
112
|
+
* T21: the same single derived position `foldRun` publishes, carried onto the
|
|
113
|
+
* wire so the board and the tooling cannot disagree about a phase's state.
|
|
114
|
+
* Optional so a row produced before this field existed stays readable.
|
|
115
|
+
*/
|
|
116
|
+
position?: PhasePosition;
|
|
75
117
|
}
|
|
76
118
|
/**
|
|
77
119
|
* The per-run wire card the recursive projection folds: the board, inspector,
|
|
@@ -93,6 +135,12 @@ export interface RecursiveRunCard {
|
|
|
93
135
|
tampers: Record<string, RecursiveTamper>;
|
|
94
136
|
/** Subagent activity (latest status wins per childId). */
|
|
95
137
|
subagents: Record<string, RecursiveSubagent>;
|
|
138
|
+
/**
|
|
139
|
+
* T18: unresolved in-flight work, DERIVED from the run directory on every fold
|
|
140
|
+
* (never stored). Optional so an older producer's card stays readable; the
|
|
141
|
+
* folder always sets it. A non-empty array explains why a lock will be refused.
|
|
142
|
+
*/
|
|
143
|
+
pendingWork?: PendingWorkItem[];
|
|
96
144
|
/** Set on recursive/run-merged; the run re-keys to this root. */
|
|
97
145
|
mergedToRepoRoot?: string;
|
|
98
146
|
}
|