@try-works/dsh-recursive-mode 0.3.1 → 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.
Files changed (117) hide show
  1. package/README.md +959 -0
  2. package/lib/client.js +9 -2
  3. package/lib/closeout-report.d.ts +113 -0
  4. package/lib/closeout-standards.d.ts +35 -0
  5. package/lib/closeout.d.ts +12 -0
  6. package/lib/commands.d.ts +1 -1
  7. package/lib/config.d.ts +202 -0
  8. package/lib/delegation.d.ts +123 -3
  9. package/lib/enforcement.d.ts +90 -1
  10. package/lib/errors.d.ts +168 -0
  11. package/lib/git-context.d.ts +17 -0
  12. package/lib/guard-log.d.ts +39 -0
  13. package/lib/handoff.d.ts +29 -0
  14. package/lib/hooks.d.ts +103 -0
  15. package/lib/identity.d.ts +61 -0
  16. package/lib/index.d.ts +32 -12
  17. package/lib/index.js +9819 -3858
  18. package/lib/job-log.d.ts +34 -0
  19. package/lib/jobs-runner.d.ts +105 -0
  20. package/lib/json-safe.d.ts +33 -0
  21. package/lib/lock.d.ts +42 -0
  22. package/lib/memory-feedback.d.ts +52 -0
  23. package/lib/memory-select.d.ts +78 -0
  24. package/lib/memory.d.ts +137 -0
  25. package/lib/model-inventory.d.ts +106 -0
  26. package/lib/phase-graph.d.ts +111 -0
  27. package/lib/phase-rules.d.ts +67 -8
  28. package/lib/plan-gate.d.ts +68 -0
  29. package/lib/policy-globs.d.ts +222 -0
  30. package/lib/policy-write.d.ts +42 -0
  31. package/lib/policy.d.ts +39 -0
  32. package/lib/recursive_ask.tool.d.ts +88 -0
  33. package/lib/recursive_closeout.tool.d.ts +1 -1
  34. package/lib/recursive_delegate.tool.d.ts +22 -0
  35. package/lib/recursive_preview.tool.d.ts +48 -0
  36. package/lib/recursive_review.tool.d.ts +28 -0
  37. package/lib/result-cap.d.ts +70 -0
  38. package/lib/review-round.d.ts +82 -0
  39. package/lib/review.d.ts +9 -0
  40. package/lib/role-route.d.ts +122 -0
  41. package/lib/router.d.ts +90 -5
  42. package/lib/runtime.d.ts +252 -12
  43. package/lib/settlement.d.ts +132 -0
  44. package/lib/skills-phase.d.ts +71 -0
  45. package/lib/status.d.ts +53 -1
  46. package/lib/teams-loop.d.ts +91 -2
  47. package/lib/training.d.ts +211 -0
  48. package/lib/ts-lint.d.ts +15 -0
  49. package/lib/types.d.ts +48 -0
  50. package/lib/workflow-audit.d.ts +207 -0
  51. package/package.json +29 -30
  52. package/preset/recursive.patch.yml +312 -0
  53. package/scripts/e2e-run.mjs +51 -0
  54. package/scripts/link-dsh.mjs +233 -0
  55. package/scripts/live/fake-llm.mjs +150 -0
  56. package/scripts/live-session-plugin.mjs +179 -0
  57. package/scripts/live-session-stock.mjs +106 -0
  58. package/scripts/live-session.mjs +139 -0
  59. package/src/client/derive.ts +18 -2
  60. package/src/closeout-report.ts +274 -0
  61. package/src/closeout-standards.ts +102 -0
  62. package/src/closeout.ts +39 -2
  63. package/src/commands.ts +116 -4
  64. package/src/config.ts +113 -0
  65. package/src/delegation.ts +336 -18
  66. package/src/enforcement.ts +262 -72
  67. package/src/errors.ts +197 -0
  68. package/src/git-context.ts +33 -2
  69. package/src/guard-log.ts +134 -0
  70. package/src/handoff.ts +62 -0
  71. package/src/hooks.ts +316 -0
  72. package/src/identity.ts +230 -0
  73. package/src/index.ts +385 -20
  74. package/src/job-log.ts +112 -0
  75. package/src/jobs-runner.ts +222 -0
  76. package/src/json-safe.ts +75 -0
  77. package/src/lock.ts +153 -16
  78. package/src/memory-feedback.ts +185 -0
  79. package/src/memory-select.ts +187 -0
  80. package/src/memory.ts +309 -0
  81. package/src/model-inventory.ts +196 -0
  82. package/src/phase-graph.ts +191 -0
  83. package/src/phase-rules.ts +236 -0
  84. package/src/plan-gate.ts +111 -0
  85. package/src/policy-globs.ts +636 -0
  86. package/src/policy-write.ts +210 -0
  87. package/src/policy.ts +70 -5
  88. package/src/recursive_ask.tool.ts +276 -0
  89. package/src/recursive_audit_team.tool.ts +7 -3
  90. package/src/recursive_closeout.tool.ts +36 -35
  91. package/src/recursive_delegate.tool.ts +194 -0
  92. package/src/recursive_init.tool.ts +4 -3
  93. package/src/recursive_lint.tool.ts +81 -6
  94. package/src/recursive_lock.tool.ts +21 -4
  95. package/src/recursive_phase.tool.ts +3 -2
  96. package/src/recursive_preview.tool.ts +142 -0
  97. package/src/recursive_review.tool.ts +190 -0
  98. package/src/recursive_scratch.tool.ts +5 -4
  99. package/src/recursive_status.tool.ts +3 -2
  100. package/src/recursive_worktree.tool.ts +6 -5
  101. package/src/result-cap.ts +130 -0
  102. package/src/review-round.ts +335 -0
  103. package/src/review.ts +17 -3
  104. package/src/role-route.ts +230 -0
  105. package/src/router.ts +128 -2
  106. package/src/runtime.ts +968 -39
  107. package/src/settlement.ts +355 -0
  108. package/src/skills-phase.ts +143 -0
  109. package/src/snapshot.ts +39 -8
  110. package/src/status.ts +209 -4
  111. package/src/teams-loop.ts +223 -9
  112. package/src/training.ts +565 -0
  113. package/src/ts-lint.ts +38 -4
  114. package/src/types.ts +51 -0
  115. package/src/workflow-audit.ts +288 -0
  116. package/scripts/install-preset.cmd +0 -7
  117. package/scripts/install-preset.js +0 -101
@@ -1,32 +1,121 @@
1
+ import { builtInToolPolicyDefault, type ToolPolicy } from './policy-globs.ts';
2
+ /**
3
+ * The BUILT-IN default rule list (T16) is defined in `src/policy-globs.ts`,
4
+ * beside the evaluator and the loader that use it as the ABSENT-file fallback,
5
+ * and re-exported here because it is part of the guard's documented behaviour.
6
+ * (It cannot live in a module of its own: the list needs the loader's predicate
7
+ * contract and the loader needs the list, which as two modules is a value-level
8
+ * import cycle — the very thing that made a top-level wiring call throw.)
9
+ */
10
+ export declare const builtInToolPolicy: typeof builtInToolPolicyDefault;
1
11
  export type EnforcementMode = 'strict' | 'advisory';
12
+ /**
13
+ * T28 — the budgets. Every unbounded loop and every unbounded result gets a named,
14
+ * configurable cap, because a bound that is not named cannot be reviewed and a bound
15
+ * that is not configurable gets hardcoded per call site (which is exactly what
16
+ * happened to `maxDepth`).
17
+ */
18
+ export interface BudgetConfig {
19
+ /** Rounds ONE phase's audit loop may run, even when every round makes progress. */
20
+ maxAuditRounds: number;
21
+ /**
22
+ * How many times a phase may be sent back for repair. Distinct from T20's
23
+ * no-progress bound: that one stops a loop REPEATING a finding, this one bounds a
24
+ * loop that keeps finding genuinely NEW things — the case the no-progress bound
25
+ * deliberately lets run.
26
+ */
27
+ maxRepairAttempts: number;
28
+ /** The ceiling for the WHOLE recursion, not a fresh allowance at each level. */
29
+ maxDelegationDepth: number;
30
+ /** Delegated children one phase may start. */
31
+ maxChildrenPerPhase: number;
32
+ /** Byte ceiling for one tool result. T24's caps bound the COUNT of findings; a few enormous findings need this. */
33
+ maxResultBytes: number;
34
+ }
35
+ export declare const DEFAULT_BUDGETS: BudgetConfig;
2
36
  export interface EnforcementConfig {
3
37
  preStep: EnforcementMode;
4
38
  toolGuards: EnforcementMode;
5
39
  tamper: EnforcementMode;
40
+ /** T28: the caps. Always present, so a caller never has to guess a default. */
41
+ budgets: BudgetConfig;
6
42
  }
7
- /** Validate the enforcement config shape (unknown keys fail at plugin load). */
43
+ /**
44
+ * Validate the enforcement config shape (unknown keys fail at plugin load).
45
+ *
46
+ * A budget must be a POSITIVE INTEGER. Zero and negatives are rejected rather than
47
+ * coerced: `0` would mean "never allowed", which bricks the phase it was meant to
48
+ * bound, and a negative would read as already-exceeded everywhere it is compared.
49
+ * A config error is loud at load, not mysterious later.
50
+ */
8
51
  export declare function resolveEnforcementConfig(config: unknown): EnforcementConfig;
9
52
  export declare const DEFAULT_ENFORCEMENT: EnforcementConfig;
53
+ /**
54
+ * T15: the rule that produced a guard decision (a machine-readable reason for
55
+ * every allow/deny/ask the guard hands back). `'none'` means no guard predicate
56
+ * fired; `'transition'` marks a decision whose only dissenting gate was the
57
+ * advisory transition gate (see consultTransitionGate).
58
+ */
59
+ export type GuardRule = 'lock-order' | 'tdd-evidence' | 'locked-write' | 'transition' | 'none';
60
+ /** T15: the transition gate's verdict as attached to a decision (advisory only). */
61
+ export interface GuardTransition {
62
+ passed: boolean;
63
+ failures: string[];
64
+ }
10
65
  /**
11
66
  * Layer 2 - tools/pre-execute guard decision.
12
67
  * Pure predicate: inspects the pending tool execution (name + args) against
13
68
  * the run tree under the given worktree root.
69
+ *
70
+ * T15: `rule` and `transition` are OPTIONAL and additive. They must stay
71
+ * optional — `coerceAskToDecision` is asserted with `toEqual({ kind: ... })`
72
+ * (an EXACT match) in tests/enforcement.spec.ts, so the coercion path may never
73
+ * grow extra keys. `evaluateToolGuard` itself always sets `rule`.
14
74
  */
15
75
  export type ToolGuardDecision = {
16
76
  kind: 'allow';
17
77
  warn?: string;
78
+ rule?: GuardRule;
79
+ transition?: GuardTransition;
18
80
  } | {
19
81
  kind: 'deny';
20
82
  reason: string;
83
+ rule?: GuardRule;
84
+ transition?: GuardTransition;
21
85
  } | {
22
86
  kind: 'ask';
23
87
  reason?: string;
88
+ rule?: GuardRule;
89
+ transition?: GuardTransition;
24
90
  };
25
91
  export interface ToolExecLike {
26
92
  name: string;
27
93
  arguments?: unknown;
28
94
  agent?: unknown;
29
95
  }
96
+ /**
97
+ * The BUILT-IN default rule list (T16) is defined in `src/policy-globs.ts`,
98
+ * beside the evaluator and the loader that use it as the ABSENT-file fallback,
99
+ * and re-exported here because it is part of the guard's documented behaviour.
100
+ * (It cannot live in a module of its own: the list needs the loader's predicate
101
+ * contract and the loader needs the list, which as two modules is a value-level
102
+ * import cycle — the very thing that made a top-level wiring call throw.)
103
+ */
104
+ /**
105
+ * The policy in force for one guard call: the worktree's policy file when
106
+ * present (a broken file fails closed), the built-in list when absent, plus the
107
+ * current phase's narrowing baseline. Exported so a reviewer — and
108
+ * `recursive:policy` later — can read the effective rules instead of inferring
109
+ * them from a code path.
110
+ */
111
+ export declare function resolveToolPolicyForGuard(worktreeRoot: string, runId: string): ToolPolicy;
112
+ /**
113
+ * The artifact whose phase baseline applies: the HIGHEST-numbered phase artifact
114
+ * present in the run (a run at phase 3 has `00`-`03` on disk). Read from the
115
+ * filesystem on every call — the same no-cache discipline the active run id
116
+ * needs, because a cached phase would apply yesterday's baseline to today's lock.
117
+ */
118
+ export declare function currentPhaseArtifact(worktreeRoot: string, runId: string): string;
30
119
  export declare function evaluateToolGuard(exec: ToolExecLike, worktreeRoot: string, activeRunId: string, mode?: EnforcementMode): ToolGuardDecision;
31
120
  /**
32
121
  * T6 (approval ask→policy bridge): an `ask` decision must never be a silent
@@ -0,0 +1,168 @@
1
+ /**
2
+ * T24 — stable, greppable, self-sufficient tool errors.
3
+ *
4
+ * WHY PROSE AND NOT A JSON ENVELOPE. A structured error envelope reaches the
5
+ * model double-escaped and is materially harder to read than a sentence, and a
6
+ * model that cannot read a refusal cannot correct it. So every error here
7
+ * renders as ONE sentence:
8
+ *
9
+ * <code> <class>: <problem>[ - <detail>]. Next: <the exact call that resolves it>.
10
+ *
11
+ * The leading `<code> <class>` pair is STABLE and greppable, so a consumer that
12
+ * is not an LLM — a test, a board badge, a log grep — can branch on it without
13
+ * parsing prose. The `Next:` clause names a real tool call, because "invalid
14
+ * input" without a route is a refusal the caller can only guess at.
15
+ *
16
+ * Codes are `RM<group><serial>` where the THIRD character is the class group:
17
+ * 1 input, 2 value, 3 workspace, 4 state, 5 runtime, 6 capability. Because the
18
+ * group is embedded in the code, a log grep for `RM1` finds every input-shaped
19
+ * failure without knowing this registry — and `tests/errors.spec.ts` asserts the
20
+ * third character matches the entry's own class, so the grouping cannot drift.
21
+ */
22
+ /** What kind of thing went wrong. Stable vocabulary — consumers branch on it. */
23
+ export type ToolErrorClass = 'input' | 'value' | 'workspace' | 'state' | 'runtime' | 'capability';
24
+ export interface ToolErrorSpec {
25
+ /** Stable `RM<group><serial>` code. Never reuse one for a different problem. */
26
+ code: string;
27
+ klass: ToolErrorClass;
28
+ /** The fixed problem statement, no trailing punctuation. */
29
+ problem: string;
30
+ /** What resolves it, naming the exact call. No trailing punctuation. */
31
+ next: string;
32
+ }
33
+ /**
34
+ * The registry. Every `recursive_*` tool error must come from here: a tool that
35
+ * invents its own sentence is a tool whose refusals cannot be grepped, and
36
+ * `tests/errors.spec.ts` asserts the codes are unique and well formed so the
37
+ * registry cannot rot into duplicates.
38
+ */
39
+ export declare const TOOL_ERRORS: {
40
+ readonly BAD_PROBE_ARGUMENTS: {
41
+ readonly code: "RM1144";
42
+ readonly klass: "input";
43
+ readonly problem: "probeArguments is not a JSON object";
44
+ readonly next: "pass a JSON object such as {\"artifact\":\"01-as-is.md\"} so the preview can evaluate the guard against real arguments";
45
+ };
46
+ readonly BAD_ASK_GATE: {
47
+ readonly code: "RM1141";
48
+ readonly klass: "input";
49
+ readonly problem: "the requested human gate is not one of tdd-mode, qa-signoff or gate-block";
50
+ readonly next: "call recursive_ask with gate: tdd-mode | qa-signoff | gate-block";
51
+ };
52
+ readonly BAD_ASK_ANSWER: {
53
+ readonly code: "RM1142";
54
+ readonly klass: "input";
55
+ readonly problem: "the answer is not one of the labels the gate offered";
56
+ readonly next: "use one of the labels the ask returned; an unoffered answer reads as a decision while being a transcription error";
57
+ };
58
+ readonly MISSING_ASK_ARTIFACT: {
59
+ readonly code: "RM1143";
60
+ readonly klass: "input";
61
+ readonly problem: "this gate has no default artifact, so one must be named";
62
+ readonly next: "pass artifact: <file> so the answer has somewhere durable to land";
63
+ };
64
+ readonly MISSING_RUN_ID: {
65
+ readonly code: "RM1101";
66
+ readonly klass: "input";
67
+ readonly problem: "runId is required";
68
+ readonly next: "call recursive_status with no runId to see the latest run id in this workspace";
69
+ };
70
+ readonly MISSING_ARTIFACT: {
71
+ readonly code: "RM1102";
72
+ readonly klass: "input";
73
+ readonly problem: "artifact is required";
74
+ readonly next: "call recursive_phase to see which artifact the run is currently on";
75
+ };
76
+ readonly MISSING_PHASE_AND_RUN: {
77
+ readonly code: "RM1103";
78
+ readonly klass: "input";
79
+ readonly problem: "phase and runId are required";
80
+ readonly next: "call recursive_status to read the run id, then pass phase as 04|05|06|07|08";
81
+ };
82
+ readonly MISSING_SCRATCH_ARGS: {
83
+ readonly code: "RM1104";
84
+ readonly klass: "input";
85
+ readonly problem: "action, runId and target are all required";
86
+ readonly next: "call recursive_scratch with action=read|write|append, a runId, and target=md|ts";
87
+ };
88
+ readonly MISSING_CREATE_RUN_ID: {
89
+ readonly code: "RM1105";
90
+ readonly klass: "input";
91
+ readonly problem: "runId is required for create";
92
+ readonly next: "call recursive_init with a runId to scaffold the run first";
93
+ };
94
+ readonly MISSING_PROMOTE_BRANCHES: {
95
+ readonly code: "RM1106";
96
+ readonly klass: "input";
97
+ readonly problem: "fromBranch and toBranch are required for promote";
98
+ readonly next: "call recursive_worktree with action=promote plus fromBranch and toBranch";
99
+ };
100
+ readonly BAD_TARGET: {
101
+ readonly code: "RM2201";
102
+ readonly klass: "value";
103
+ readonly problem: "target must be md or ts";
104
+ readonly next: "pass target=md for /.recursive/run/<id>/scratch/scratch.md or target=ts for scratch.ts";
105
+ };
106
+ readonly BAD_ACTION: {
107
+ readonly code: "RM2202";
108
+ readonly klass: "value";
109
+ readonly problem: "action must be create | promote | status";
110
+ readonly next: "pass action=create, action=promote or action=status";
111
+ };
112
+ readonly NO_WORKSPACE: {
113
+ readonly code: "RM3301";
114
+ readonly klass: "workspace";
115
+ readonly problem: "this session is not attached to a registered workspace";
116
+ readonly next: "open the session inside a workspace directory; the control-plane root is resolved from the session cwd";
117
+ };
118
+ readonly NO_RUN: {
119
+ readonly code: "RM4401";
120
+ readonly klass: "state";
121
+ readonly problem: "no recursive run exists in this workspace";
122
+ readonly next: "call recursive_init with a runId to scaffold the first run";
123
+ };
124
+ readonly NO_PHASE: {
125
+ readonly code: "RM4402";
126
+ readonly klass: "state";
127
+ readonly problem: "no current recursive phase could be determined";
128
+ readonly next: "call recursive_init to scaffold the run, or recursive_status to inspect why every phase is locked";
129
+ };
130
+ readonly PENDING_WORK: {
131
+ readonly code: "RM4403";
132
+ readonly klass: "state";
133
+ readonly problem: "the run has unresolved delegated work, so this phase cannot lock yet";
134
+ readonly next: "call recursive_status to see the pending delegation, have the child write its reply.md, then lock again";
135
+ };
136
+ readonly RUNTIME_REFUSED: {
137
+ readonly code: "RM5501";
138
+ readonly klass: "runtime";
139
+ readonly problem: "the recursive runtime refused the operation";
140
+ readonly next: "fix the cause named in the detail and retry; a gate refusal names the artifact and its status";
141
+ };
142
+ readonly TEAM_SERVICE_UNAVAILABLE: {
143
+ readonly code: "RM6601";
144
+ readonly klass: "capability";
145
+ readonly problem: "the agent-teams service is not available in this composition";
146
+ readonly next: "use recursive_lock and recursive_lint directly, or mount a composition that provides ctx.agentTeams";
147
+ };
148
+ };
149
+ export type ToolErrorName = keyof typeof TOOL_ERRORS;
150
+ /**
151
+ * Render one registry entry as the single sentence a tool returns.
152
+ * `detail` carries the run-specific part (a run id, an artifact, a gate).
153
+ */
154
+ export declare function toolError(name: ToolErrorName, detail?: string): string;
155
+ /** True when a string already carries a registry code — used to avoid double-wrapping. */
156
+ export declare function hasToolErrorCode(message: string): boolean;
157
+ /**
158
+ * Give a thrown runtime message a stable code WITHOUT nesting one that is
159
+ * already there.
160
+ *
161
+ * A refusal the runtime already expressed in this registry's vocabulary (for
162
+ * example `RM4403` pending work) is passed through untouched: wrapping it would
163
+ * bury the real code inside `RM5501`'s detail and make the greppable handle
164
+ * useless, which is the whole reason the registry exists. A bare thrown message
165
+ * has no code to branch on, so it is wrapped and its sentence survives as the
166
+ * detail.
167
+ */
168
+ export declare function codeRuntimeRefusal(message: string): string;
@@ -51,3 +51,20 @@ export declare function verifyWorktreeBranch(repoRoot: string, recordedBranch: s
51
51
  ok: boolean;
52
52
  reason: string | null;
53
53
  };
54
+ /**
55
+ * FU-4 — the paths this checkout has actually changed, for the memory loader's path weighting.
56
+ *
57
+ * WHY IT EXISTS. T29's selection weights a shard that names a path the run has changed **above** one that
58
+ * merely shares wording with the query — but nothing computed those paths, so the weighting was exercised
59
+ * by tests and never by a run. A weighting that is never fed is a rule that does not exist in production.
60
+ *
61
+ * ⚠ IT REUSES `gitRun` AND THE SAME TWO QUERIES `ts-lint.ts` ALREADY USES (`diff --name-only` for tracked
62
+ * changes, `ls-files --others` for new files). A second way of asking git the same question is a second
63
+ * thing to get subtly wrong — and the lint path is the one with a parity golden behind it.
64
+ *
65
+ * ⚠ IT IS CAPPED, AND IT IS BEST-EFFORT. Uncapped, a large checkout would let path matches swamp the
66
+ * query score entirely, so the list is capped; and a repo where git fails returns `[]` rather than
67
+ * throwing, because a memory hint must never be the reason a phase call fails.
68
+ */
69
+ export declare const MAX_CHANGED_PATHS = 50;
70
+ export declare function changedPaths(repoRoot: string, limit?: number): string[];
@@ -0,0 +1,39 @@
1
+ import type { GuardRule } from './enforcement.ts';
2
+ /**
3
+ * One logged guard decision (the JSONL record shape the board/tests read).
4
+ * `rule` is always set (the guard's own machine-readable reason for the
5
+ * verdict); `transition` is present whenever the transition gate was consulted.
6
+ */
7
+ export interface GuardDecisionRecord {
8
+ at: string;
9
+ runId: string;
10
+ tool: string;
11
+ kind: 'allow' | 'deny' | 'ask';
12
+ rule: GuardRule;
13
+ reason?: string;
14
+ transition?: {
15
+ passed: boolean;
16
+ failures: string[];
17
+ };
18
+ }
19
+ /** One logged observed-write tamper (a LOCKED artifact whose hash no longer matches). */
20
+ export interface ObservedTamperRecord {
21
+ at: string;
22
+ runId: string;
23
+ path: string;
24
+ reason: string;
25
+ }
26
+ /** Newest-N retention cap for both logs (rewrite-on-exceed, never unbounded). */
27
+ export declare const GUARD_LOG_MAX_RECORDS = 500;
28
+ /** The rolling decision log path under the control-plane root. */
29
+ export declare function guardDecisionLogPath(root: string): string;
30
+ /** The rolling observed-tamper log path under the control-plane root. */
31
+ export declare function observedTamperLogPath(root: string): string;
32
+ /** Log one guard decision (allows included — the trace shows what, and why). */
33
+ export declare function appendGuardDecision(root: string, record: GuardDecisionRecord): void;
34
+ /** Log one observed-write tamper. */
35
+ export declare function appendObservedTamper(root: string, record: ObservedTamperRecord): void;
36
+ /** The newest `limit` guard decisions for `root`, newest first. Never throws. */
37
+ export declare function readGuardDecisions(root: string, limit?: number): GuardDecisionRecord[];
38
+ /** The newest `limit` observed tampers for `root`, newest first. Never throws. */
39
+ export declare function readObservedTampers(root: string, limit?: number): ObservedTamperRecord[];
package/lib/handoff.d.ts CHANGED
@@ -30,6 +30,35 @@ export declare function replyPath(input: {
30
30
  delegationId: string;
31
31
  childId: string;
32
32
  }): string;
33
+ /**
34
+ * ⚠ FU-17 — THE WORK BRIEF'S SLICE: what a child is told when it is delegated the phase's ACTUAL WORK rather
35
+ * than a review of it.
36
+ *
37
+ * WHY THIS IS SEPARATE FROM THE REVIEWER'S SLICE, and the reason is not tidiness. A reviewer is told what to
38
+ * look FOR — anti-patterns, a verdict vocabulary, "do not be satisfied by prose". A worker must be told what to
39
+ * PRODUCE, and above all **the standard its output will be judged against**, because the phase artifact is
40
+ * linted for required sections and a child that was never told them cannot meet them. That standard already
41
+ * exists in one place (`getArtifactRequiredSections`), so this composes it rather than restating it — a second
42
+ * copy would drift from the linter, which is the defect this project has fixed more than once.
43
+ *
44
+ * ⚠ IT ALSO TELLS THE CHILD WHO DECIDES. The main agent verifies and records what it accepted
45
+ * (`## Subagent Contribution Verification`: reviewed action records, main-agent verification performed, an
46
+ * acceptance decision, refresh handling, repair performed). Saying so up front is not politeness: a child that
47
+ * believes its own output is final writes a different, worse submission than one that knows a parent will
48
+ * check it against named sections.
49
+ */
50
+ export declare function buildWorkSlice(input: {
51
+ /** The main agent's task for this child, verbatim. */
52
+ instruction: string;
53
+ /** The run-relative artifact the work contributes to, e.g. `03-implementation-summary.md`. */
54
+ artifactFile: string;
55
+ /** The phase key, for the brief's own traceability back to the run. */
56
+ phase: string;
57
+ /** Required sections for that artifact — pass `getArtifactRequiredSections(artifactFile, profile)`. */
58
+ requiredSections: readonly string[];
59
+ /** Optional: the phase's lint rules, so the child sees the gate fields too. */
60
+ lintNotes?: readonly string[];
61
+ }): string;
33
62
  /** Child-scoped disposable scratch (Phase B R5, PROPOSAL 10.7). */
34
63
  export declare function childScratchPath(input: {
35
64
  root: string;
package/lib/hooks.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ /** The five named points, mapped onto DSH seams (see the module comment). */
2
+ export declare const HOOK_POINTS: readonly ["pre_turn", "pre_generate", "post_generate", "pre_trigger", "post_trigger"];
3
+ export type HookPoint = (typeof HOOK_POINTS)[number];
4
+ /** What a hook decided. `hold` means "stop and wait", distinct from a refusal. */
5
+ export type HookDecision = 'continue' | 'deny' | 'hold';
6
+ /**
7
+ * Points that may STOP the work, versus points that only observe it. A deny from an
8
+ * observing point is downgraded, because the thing it would veto has already happened.
9
+ */
10
+ export declare const GATING_POINTS: readonly HookPoint[];
11
+ export declare const OBSERVING_POINTS: readonly HookPoint[];
12
+ export declare function isGating(point: HookPoint): boolean;
13
+ export declare function isObserving(point: HookPoint): boolean;
14
+ /** What a hook returns. `void` means "no opinion", i.e. continue. */
15
+ export interface HookOutcome {
16
+ decision: HookDecision;
17
+ /** Why — required in spirit for a deny or a hold, and carried verbatim. */
18
+ reason?: string;
19
+ /**
20
+ * What the hook CHANGED or learned, returned rather than mutated in place. Silent
21
+ * mutation leaves the next reader nothing to find.
22
+ */
23
+ annotations?: Record<string, unknown>;
24
+ }
25
+ export interface HookRunContext {
26
+ readonly point: HookPoint;
27
+ /** The binding's own deadline in ms, so a hook can bail before it is cut off. */
28
+ readonly timeoutMs: number;
29
+ /** Monotonic-ish clock from the registry, for a hook that wants to time itself. */
30
+ readonly now: () => number;
31
+ }
32
+ export interface Hook<I = unknown> {
33
+ /** Stable name: it is what the audit trail and the board show. */
34
+ readonly name: string;
35
+ /**
36
+ * Higher runs FIRST. Ties break by REGISTRATION ORDER, which is what makes the
37
+ * chain reproducible — an ordering that depends on object key order or on
38
+ * scheduling is an ordering nobody can reason about.
39
+ */
40
+ readonly priority: number;
41
+ /** Per-binding deadline; falls back to the point's default. */
42
+ readonly timeoutMs?: number;
43
+ /**
44
+ * What to do when this hook throws or times out. Defaults by POINT: fail_closed on
45
+ * a gating point, fail_open on an observing one.
46
+ */
47
+ readonly onError?: 'fail_closed' | 'fail_open';
48
+ readonly run: (input: I, context: HookRunContext) => HookOutcome | void | Promise<HookOutcome | void>;
49
+ }
50
+ /** One hook's contribution to a chain run — the audit trail. */
51
+ export interface HookRunRecord {
52
+ name: string;
53
+ decision: HookDecision;
54
+ durationMs: number;
55
+ /** Present when the hook failed or timed out; the chain's policy decided what next. */
56
+ error?: string;
57
+ annotations?: Record<string, unknown>;
58
+ /** True when the hook's own decision was overridden (an observing point denying). */
59
+ downgraded?: boolean;
60
+ }
61
+ export interface HookChainResult {
62
+ point: HookPoint;
63
+ decision: HookDecision;
64
+ reason?: string;
65
+ /** Every hook that ran, in order; hooks after a short-circuit are absent by design. */
66
+ ran: HookRunRecord[];
67
+ }
68
+ /** Registry configuration: per-point deadlines. */
69
+ export interface HookRegistryOptions {
70
+ /** Default deadline per point when a binding does not set one. */
71
+ timeoutMs?: Partial<Record<HookPoint, number>>;
72
+ /** Clock injection, so a test can drive timeouts without sleeping. */
73
+ now?: () => number;
74
+ }
75
+ export interface HookRegistry {
76
+ readonly points: readonly HookPoint[];
77
+ /** Register one hook. Returns a disposer, so a sibling can withdraw cleanly. */
78
+ register<I>(point: HookPoint, hook: Hook<I>): () => void;
79
+ /** Run the chain for a point. Never throws: failure policy decides the outcome. */
80
+ run<I>(point: HookPoint, input: I): Promise<HookChainResult>;
81
+ /** The registered chain, in execution order — what the board would show. */
82
+ list(point?: HookPoint): Array<{
83
+ point: HookPoint;
84
+ name: string;
85
+ priority: number;
86
+ onError: 'fail_closed' | 'fail_open';
87
+ timeoutMs: number;
88
+ }>;
89
+ clear(point?: HookPoint): void;
90
+ }
91
+ /**
92
+ * Build a registry. `now` is injectable so a timeout is testable without waiting for
93
+ * one: real deadlines in a test are a flake waiting to happen, which this codebase has
94
+ * already paid for once.
95
+ */
96
+ export declare function createHookRegistry(options?: HookRegistryOptions): HookRegistry;
97
+ /** A stable fingerprint of one hook binding, for a board that lists the chain. */
98
+ export declare function hookFingerprint(entry: {
99
+ point: HookPoint;
100
+ name: string;
101
+ priority: number;
102
+ onError: string;
103
+ }): string;
@@ -0,0 +1,61 @@
1
+ /** Canonical forms at or below this size are inlined into the key; larger ones are digested. */
2
+ export declare const INLINE_LIMIT_BYTES = 2048;
3
+ /**
4
+ * Canonical JSON for an input: object keys sorted at every depth, arrays in order.
5
+ * Throws for input with no honest canonical form (a lone surrogate, a non-finite
6
+ * number, `undefined`) rather than silently substituting something hashable.
7
+ */
8
+ export declare function canonicalInput(value: unknown): string;
9
+ /**
10
+ * The bounded key for one input: the canonical form inline when it is small, and a
11
+ * digest plus byte length when it is not. The length is kept so a truncated input
12
+ * cannot collide with a genuinely different one of the same digest.
13
+ */
14
+ export declare function idempotencyKey(input: unknown): string;
15
+ /**
16
+ * The deterministic id of an operation: same act and same input give the same id,
17
+ * whatever the key order, and a materially different operation never does.
18
+ */
19
+ export declare function operationId(input: {
20
+ act: string;
21
+ input: unknown;
22
+ }): string;
23
+ /** One recorded attempt. The index holds only what makes a retry recognisable. */
24
+ export interface OperationRecord {
25
+ id: string;
26
+ act: string;
27
+ /** When the attempt was recorded (caller-supplied, so a test can be deterministic). */
28
+ at: string;
29
+ /** What happened, when the caller knows: e.g. `applied`, `refused`, `interrupted`. */
30
+ outcome?: string;
31
+ /**
32
+ * T28: the phase this operation belongs to, when it has one.
33
+ *
34
+ * Recorded so a budget can be counted FROM the index rather than tracked
35
+ * separately — the children-per-phase cap is `readOperations(...)` filtered by
36
+ * phase, which needs no new state and cannot drift from the operations it counts.
37
+ */
38
+ phase?: string;
39
+ }
40
+ /** The run-scoped, append-only operation index. Bounded by use, git-ignored with the run. */
41
+ export declare function operationsPath(runDir: string): string;
42
+ /** Every recorded attempt, oldest first. Never throws; a corrupt line is skipped. */
43
+ export declare function readOperations(runDir: string): OperationRecord[];
44
+ /** The LATEST record for an id, or null when the operation has never been attempted. */
45
+ export declare function findOperation(runDir: string, id: string): OperationRecord | null;
46
+ /** Append one attempt. Best-effort: a failed write must never change an operation's outcome. */
47
+ export declare function recordOperation(runDir: string, record: OperationRecord): boolean;
48
+ /** True when this exact operation has already been attempted in this run. */
49
+ export declare function wasAttempted(runDir: string, id: string): boolean;
50
+ /** True when the index exists at all — lets a caller distinguish "no retry" from "no index". */
51
+ export declare function hasIndex(runDir: string): boolean;
52
+ /**
53
+ * T28: how many DISTINCT operations of one act have been recorded for one phase.
54
+ *
55
+ * Counted from the index rather than tracked in parallel, so a budget cannot drift
56
+ * away from the operations it bounds. DISTINCT ids, not records, because a resumed
57
+ * turn re-records the SAME operation: counting records would make a long review look
58
+ * like many children and fire the cap on legitimate work, while a genuine new
59
+ * operation (a repaired artifact changes the body, hence the id) still counts.
60
+ */
61
+ export declare function countOperations(runDir: string, act: string, phase: string): number;
package/lib/index.d.ts CHANGED
@@ -1,5 +1,36 @@
1
1
  import { type Context } from '@deepseek-ai/cordis';
2
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
3
+ import type { RecursiveModeConfig } from './config.ts';
4
+ /**
5
+ * rc.2 rebase (T31a) — the message-source vocabulary changed under us.
6
+ *
7
+ * At `dsh-v0.1.1-rc.2` `MessageSourceMap` carried a shared catch-all
8
+ * `plugin: { kind: 'plugin'; plugin: string }` entry, which this file used for
9
+ * its injected phase-lint reminder. At `dsh-v0.2.0-rc.2` that entry is GONE:
10
+ * the map is merge-extensible and, in its own words, "each producer declares
11
+ * its own `kind` in its own module; there is no shared catch-all `plugin`
12
+ * kind". This was invisible in the old checkout because its `node_modules`
13
+ * still held a stale `dsh-llm`.
14
+ *
15
+ * The idiom below is copied from the shipped `@deepseek-ai/dsh-repeat-tool-reminder`,
16
+ * whose pre-step reminder is the closest analogue to ours: a user-role message
17
+ * whose source declares its own kind and a `form: 'notice'` one-line account.
18
+ */
19
+ declare module '@deepseek-ai/dsh-llm' {
20
+ interface MessageSourceMap {
21
+ 'recursive-mode': {
22
+ kind: 'recursive-mode';
23
+ } & ContextFormed;
24
+ }
25
+ }
2
26
  export declare const name = "@try-works/dsh-recursive-mode";
27
+ /**
28
+ * T7: the plugin's Config schema, which the settings service DISCOVERS (see `src/config.ts`
29
+ * for why declaring it is the registration). Re-exported from the entry because the Loader
30
+ * reads it from the plugin module.
31
+ */
32
+ export { Config } from './config.ts';
33
+ export type { RecursiveModeConfig } from './config.ts';
3
34
  export { RecursiveRuntime } from './runtime.ts';
4
35
  export { createRecursiveStatusTool } from './recursive_status.tool.ts';
5
36
  export { createRecursiveInitTool } from './recursive_init.tool.ts';
@@ -31,15 +62,4 @@ export * from './skills.ts';
31
62
  * read-path tools (status/init/lock/lint) are registered through it (R2/R4).
32
63
  */
33
64
  export declare const inject: string[];
34
- /**
35
- * Plugin entry (SP2 R1). Stage A (mount-time, this apply): register the
36
- * isolated ctx.recursive service + the recursive_* tools + the /recursive
37
- * command + the recursive:policy prompt section + the LIVE board/strip route
38
- * (HTTP state + SSE), served per-workspace from the filesystem fold. No
39
- * repo/run work and NO session-event emission here: zero recursive/* events
40
- * are ever appended (resume-crash fix), and the board reads the live fs route.
41
- */
42
- export declare function apply(ctx: Context, config?: {
43
- shellOnly?: boolean;
44
- repoRoot?: string;
45
- }): void;
65
+ export declare function apply(ctx: Context, config?: RecursiveModeConfig): void;