@try-works/dsh-recursive-mode 0.2.4 → 0.3.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/lib/delegation.d.ts +168 -0
- package/lib/enforcement.d.ts +8 -0
- package/lib/goals-projection.d.ts +92 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +773 -9
- package/lib/recursive_audit_team.tool.d.ts +2 -0
- package/lib/runtime.d.ts +112 -3
- package/lib/teams-loop.d.ts +160 -0
- package/package.json +1 -1
- package/scripts/test-recursive-mode-smoke.ts +45 -19
- package/src/delegation.ts +337 -1
- package/src/enforcement.ts +16 -1
- package/src/goals-projection.ts +149 -0
- package/src/index.ts +41 -11
- package/src/recursive_audit_team.tool.ts +141 -0
- package/src/runtime.ts +134 -6
- package/src/teams-loop.ts +259 -0
package/lib/runtime.d.ts
CHANGED
|
@@ -5,10 +5,13 @@ import { type PhaseRules } from './phase-rules.ts';
|
|
|
5
5
|
import { type ScratchTarget } from './scratch.ts';
|
|
6
6
|
import { type ReviewBundleInput } from './review.ts';
|
|
7
7
|
import { type SubagentProviderLike, type RouteDecision, type CapabilityProbe } from './router.ts';
|
|
8
|
-
import { type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference } from './delegation.ts';
|
|
8
|
+
import { type SubagentsRuntimeLike, type SubagentStartRequestLike, type SubagentResultLike, type Reference, type SubagentParentHandle } from './delegation.ts';
|
|
9
9
|
import { type RecursivePhaseState } from './lifecycle.ts';
|
|
10
10
|
import { type EnforcementConfig, type ToolGuardDecision, type ToolExecLike } from './enforcement.ts';
|
|
11
11
|
import { type CreateWorktreeResult, type PromoteBranchResult } from './worktree.ts';
|
|
12
|
+
import { syncRunGoal, type GoalServiceLike } from './goals-projection.ts';
|
|
13
|
+
import { type TeamRuntimeLike, type AuditToPassResult, type TeamCallerHandle, type TeamTaskViewLike, type AuditRoundOutcome } from './teams-loop.ts';
|
|
14
|
+
import type { ContinuableChildId, ContinuableMessageId } from './delegation.ts';
|
|
12
15
|
declare module '@deepseek-ai/cordis' {
|
|
13
16
|
interface Context {
|
|
14
17
|
recursive: RecursiveRuntime;
|
|
@@ -35,10 +38,98 @@ export declare class RecursiveRuntime extends Service {
|
|
|
35
38
|
constructor(ctx: Context, config?: {
|
|
36
39
|
repoRoot?: string;
|
|
37
40
|
workspaceRegistry?: WorkspaceRegistryLike;
|
|
41
|
+
goals?: GoalServiceLike | null;
|
|
38
42
|
});
|
|
39
43
|
private readonly repoRoot;
|
|
40
44
|
private readonly workspaceRegistry;
|
|
45
|
+
private readonly goalsService;
|
|
41
46
|
private _enforcementConfig;
|
|
47
|
+
/**
|
|
48
|
+
* T3 (agentTeams task loop): run the audit→repair→re-audit state machine on
|
|
49
|
+
* ONE durable team task. The `teams` seam (live `ctx.agentTeams`) is injected
|
|
50
|
+
* per-call so the loop stays unit-testable; `runAuditRound` is the caller's
|
|
51
|
+
* round executor (live usage wires T4's continuable delegation). Locking the
|
|
52
|
+
* phase artifact is `lockPhase` — the loop NEVER locks before an APPROVE.
|
|
53
|
+
*/
|
|
54
|
+
auditToPass(input: {
|
|
55
|
+
teams: TeamRuntimeLike;
|
|
56
|
+
caller: TeamCallerHandle;
|
|
57
|
+
root: string;
|
|
58
|
+
runId: string;
|
|
59
|
+
phase: string;
|
|
60
|
+
artifact: string;
|
|
61
|
+
agent?: {
|
|
62
|
+
session?: {
|
|
63
|
+
header?: {
|
|
64
|
+
cwd?: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
} | null;
|
|
68
|
+
runAuditRound: (round: number, task: TeamTaskViewLike) => Promise<AuditRoundOutcome>;
|
|
69
|
+
blockedBy?: readonly string[];
|
|
70
|
+
writeScopes?: readonly string[];
|
|
71
|
+
reviewerName?: string;
|
|
72
|
+
maxRounds?: number;
|
|
73
|
+
waitTimeoutMs?: number;
|
|
74
|
+
}): Promise<AuditToPassResult & {
|
|
75
|
+
history?: string;
|
|
76
|
+
lock?: LockArtifactResult;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* T1 (goals projection): project the run into the native goals service so it is
|
|
80
|
+
* a first-class durable, resumable, blockable object. Best-effort — the run's
|
|
81
|
+
* filesystem state is the source of truth; a goal is the durable projection.
|
|
82
|
+
*/
|
|
83
|
+
projectRunToGoal(agent: {
|
|
84
|
+
session?: {
|
|
85
|
+
header?: {
|
|
86
|
+
cwd?: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
} | null | undefined, runId: string, state?: Parameters<typeof syncRunGoal>[3]): {
|
|
90
|
+
ok: true;
|
|
91
|
+
phase: import("./goals-projection.ts").GoalPhase;
|
|
92
|
+
ref?: import("./goals-projection.ts").GoalRefLike;
|
|
93
|
+
created?: boolean;
|
|
94
|
+
} | {
|
|
95
|
+
ok: boolean;
|
|
96
|
+
reason: string;
|
|
97
|
+
};
|
|
98
|
+
/** T1: block the run's goal on a gate-block (durable + UI-visible). */
|
|
99
|
+
blockRunToGoal(agent: {
|
|
100
|
+
session?: {
|
|
101
|
+
header?: {
|
|
102
|
+
cwd?: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
} | null | undefined, runId: string, reason: {
|
|
106
|
+
code: string;
|
|
107
|
+
message: string;
|
|
108
|
+
}): {
|
|
109
|
+
ok: true;
|
|
110
|
+
phase: import("./goals-projection.ts").GoalPhase;
|
|
111
|
+
ref?: import("./goals-projection.ts").GoalRefLike;
|
|
112
|
+
created?: boolean;
|
|
113
|
+
} | {
|
|
114
|
+
ok: boolean;
|
|
115
|
+
reason: string;
|
|
116
|
+
};
|
|
117
|
+
/** T1: re-arm the run's goal on a reopen (blocked/paused -> active). */
|
|
118
|
+
resumeRunToGoal(agent: {
|
|
119
|
+
session?: {
|
|
120
|
+
header?: {
|
|
121
|
+
cwd?: string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
} | null | undefined, runId: string): {
|
|
125
|
+
ok: true;
|
|
126
|
+
phase: import("./goals-projection.ts").GoalPhase;
|
|
127
|
+
ref?: import("./goals-projection.ts").GoalRefLike;
|
|
128
|
+
created?: boolean;
|
|
129
|
+
} | {
|
|
130
|
+
ok: boolean;
|
|
131
|
+
reason: string;
|
|
132
|
+
};
|
|
42
133
|
/**
|
|
43
134
|
* Workspace-scoped control-plane root (R1 binding invariant).
|
|
44
135
|
* Resolves the session agent's canonical cwd -> workspace path via the
|
|
@@ -94,8 +185,16 @@ export declare class RecursiveRuntime extends Service {
|
|
|
94
185
|
/**
|
|
95
186
|
* Phase B (native delegation): build a review bundle (R1) + file-backed
|
|
96
187
|
* handoff docs (R2), resolve the role via the router policy (R3), and call
|
|
97
|
-
* ctx.subagents
|
|
98
|
-
*
|
|
188
|
+
* ctx.subagents with the full request (R4). Workspace-scoped: every path
|
|
189
|
+
* resolves under the session's control-plane root.
|
|
190
|
+
*
|
|
191
|
+
* `mode: 'continuable'` (T4) runs the audit→repair→re-audit loop on ONE
|
|
192
|
+
* durable continuable child (startContinuable → followup with the repair
|
|
193
|
+
* instruction → settle) and drains the child on closeout. It requires an
|
|
194
|
+
* `awaitRoundResult` observer (the parent-side settlement seam) AND the exact
|
|
195
|
+
* live `parent` Agent (continuable followup is object-identity authority);
|
|
196
|
+
* when either is absent it falls back to one-shot `delegate()` with a flag —
|
|
197
|
+
* never silently. One-shot `start()` is never called on the continuable path.
|
|
99
198
|
*/
|
|
100
199
|
delegateReview(input: {
|
|
101
200
|
root: string;
|
|
@@ -116,6 +215,11 @@ export declare class RecursiveRuntime extends Service {
|
|
|
116
215
|
subagents?: SubagentsRuntimeLike;
|
|
117
216
|
maxDepth?: number;
|
|
118
217
|
toolFilter?: unknown;
|
|
218
|
+
mode?: 'one-shot' | 'continuable';
|
|
219
|
+
awaitRoundResult?: (childId: ContinuableChildId, messageId: ContinuableMessageId) => Promise<SubagentResultLike | null>;
|
|
220
|
+
maxRounds?: number;
|
|
221
|
+
/** T4: the exact live direct-parent Agent (object-identity authority). */
|
|
222
|
+
parent?: SubagentParentHandle;
|
|
119
223
|
}): Promise<{
|
|
120
224
|
decision: RouteDecision;
|
|
121
225
|
probe: CapabilityProbe;
|
|
@@ -133,6 +237,11 @@ export declare class RecursiveRuntime extends Service {
|
|
|
133
237
|
};
|
|
134
238
|
actionRecordPath: string;
|
|
135
239
|
error: string | null;
|
|
240
|
+
continuable: {
|
|
241
|
+
rounds: import("./delegation.ts").ContinuableRoundLike[];
|
|
242
|
+
childId: string | undefined;
|
|
243
|
+
fellBackToOneShot: boolean | undefined;
|
|
244
|
+
} | null;
|
|
136
245
|
}>;
|
|
137
246
|
/** R6: validate a child's claimed references against actual files. */
|
|
138
247
|
validateReferences(root: string, references: Reference[]): import("./delegation.ts").ReferenceCheck;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T3: model the audit→repair→re-audit loop as agentTeams Tasks.
|
|
3
|
+
*
|
|
4
|
+
* The recursion concept's core state machine, expressed over the native
|
|
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
|
|
8
|
+
* updateTask(complete) → lock. A stuck reviewer is interrupted through the
|
|
9
|
+
* team's own kill switch; a REJECT or round-cap releases the task and fails
|
|
10
|
+
* loud — a lock NEVER happens before an APPROVE verdict.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is a structural seam (TeamRuntimeLike / TeamTaskViewLike)
|
|
13
|
+
* so the whole loop is unit-testable with a fake team; the live `agentTeams`
|
|
14
|
+
* service satisfies the seam without an adapter. The caller Agent passes
|
|
15
|
+
* through as an opaque `TeamCallerHandle` (never serialized; workspace-scoped
|
|
16
|
+
* invariant).
|
|
17
|
+
*/
|
|
18
|
+
/** Verdict vocabulary shared with T4 (matches the delegated review schema). */
|
|
19
|
+
export type AuditVerdict = 'APPROVE' | 'REVISE' | 'REJECT';
|
|
20
|
+
/**
|
|
21
|
+
* Opaque handle to the live Team member/lead Agent authorizing task mutations.
|
|
22
|
+
* The seam uses it only for identity/authority and never inspects or serializes it.
|
|
23
|
+
*/
|
|
24
|
+
export interface TeamCallerHandle {
|
|
25
|
+
readonly id?: string;
|
|
26
|
+
readonly session?: {
|
|
27
|
+
readonly header?: {
|
|
28
|
+
readonly cwd?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Minimal cancellation shape (a live AbortSignal satisfies it). */
|
|
33
|
+
export interface TeamAbortSignalLike {
|
|
34
|
+
readonly throwIfAborted: () => void;
|
|
35
|
+
}
|
|
36
|
+
/** Task identity: a branded string in the live service. */
|
|
37
|
+
export type TeamTaskIdLike = string;
|
|
38
|
+
/** Task status vocabulary (mirrors the live TeamTaskStatus). */
|
|
39
|
+
export type TeamTaskStatusLike = 'pending' | 'in_progress' | 'completed' | 'deleted';
|
|
40
|
+
/** Task action vocabulary (mirrors the live TeamTaskAction; the loop uses a subset). */
|
|
41
|
+
export type TeamTaskActionLike = 'claim' | 'release' | 'edit' | 'complete';
|
|
42
|
+
/** One runtime-enriched task view (the fields the loop reads/writes). */
|
|
43
|
+
export interface TeamTaskViewLike {
|
|
44
|
+
readonly id: TeamTaskIdLike;
|
|
45
|
+
readonly revision: number;
|
|
46
|
+
readonly subject: string;
|
|
47
|
+
readonly description: string;
|
|
48
|
+
readonly status: TeamTaskStatusLike;
|
|
49
|
+
readonly blockedBy: TeamTaskIdLike[];
|
|
50
|
+
readonly writeScopes: string[];
|
|
51
|
+
readonly ownerName?: string;
|
|
52
|
+
readonly ready: boolean;
|
|
53
|
+
readonly writeScopeWarnings: string[];
|
|
54
|
+
}
|
|
55
|
+
/** A wait observation (mirrors TeamWaitResult). */
|
|
56
|
+
export interface TeamWaitResultLike {
|
|
57
|
+
readonly timedOut: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Create-task request (mirrors CreateTeamTaskRequest). */
|
|
60
|
+
export interface CreateTeamTaskRequestLike {
|
|
61
|
+
subject: string;
|
|
62
|
+
description: string;
|
|
63
|
+
blockedBy?: readonly TeamTaskIdLike[];
|
|
64
|
+
writeScopes?: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
/** Compare-and-set task transition (mirrors UpdateTeamTaskRequest). */
|
|
67
|
+
export interface UpdateTeamTaskRequestLike {
|
|
68
|
+
taskId: TeamTaskIdLike;
|
|
69
|
+
expectedRevision: number;
|
|
70
|
+
action: TeamTaskActionLike;
|
|
71
|
+
subject?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
blockedBy?: readonly TeamTaskIdLike[];
|
|
74
|
+
writeScopes?: readonly string[];
|
|
75
|
+
owner?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The agentTeams seam the loop calls. `waitForChange`/`interrupt`/`getTask`/
|
|
79
|
+
* `listTasks` are optional (loops degrade: no wait, no kill switch, no board
|
|
80
|
+
* re-read) — `createTask`/`updateTask` are hard requirements.
|
|
81
|
+
*/
|
|
82
|
+
export interface TeamRuntimeLike {
|
|
83
|
+
createTask(caller: TeamCallerHandle, request: CreateTeamTaskRequestLike): Promise<TeamTaskViewLike>;
|
|
84
|
+
updateTask(caller: TeamCallerHandle, request: UpdateTeamTaskRequestLike): Promise<TeamTaskViewLike>;
|
|
85
|
+
getTask?(caller: TeamCallerHandle, id: TeamTaskIdLike): TeamTaskViewLike;
|
|
86
|
+
listTasks?(caller: TeamCallerHandle): TeamTaskViewLike[];
|
|
87
|
+
waitForChange?(caller: TeamCallerHandle, timeoutMs: number, signal: TeamAbortSignalLike | undefined): Promise<TeamWaitResultLike>;
|
|
88
|
+
interrupt?(caller: TeamCallerHandle, targetName: string): {
|
|
89
|
+
previousStatus: 'running' | 'idle' | 'inactive';
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** One audit round's outcome (the verdict + synthesized repair + acceptance). */
|
|
93
|
+
export interface AuditRoundOutcome {
|
|
94
|
+
readonly verdict: AuditVerdict;
|
|
95
|
+
/** Repair instruction synthesized from findings (REVISE only). */
|
|
96
|
+
readonly repair?: string;
|
|
97
|
+
/** Whether the underlying delegation result itself was accepted. */
|
|
98
|
+
readonly accepted: boolean;
|
|
99
|
+
readonly reason?: string;
|
|
100
|
+
}
|
|
101
|
+
/** One completed loop round (the task revision trail for the board history). */
|
|
102
|
+
export interface AuditLoopRound {
|
|
103
|
+
readonly round: number;
|
|
104
|
+
readonly verdict: AuditVerdict;
|
|
105
|
+
readonly repair?: string;
|
|
106
|
+
readonly taskRevision: number;
|
|
107
|
+
}
|
|
108
|
+
/** The auditToPass result. */
|
|
109
|
+
export interface AuditToPassResult {
|
|
110
|
+
readonly ok: boolean;
|
|
111
|
+
readonly reason?: string;
|
|
112
|
+
/** The durable task the loop ran on (revision trail lives on the team log). */
|
|
113
|
+
readonly taskId?: TeamTaskIdLike;
|
|
114
|
+
readonly rounds: AuditLoopRound[];
|
|
115
|
+
/** True only when an APPROVE verdict completed the task and locked the phase. */
|
|
116
|
+
readonly locked: boolean;
|
|
117
|
+
/** Latest task view (board-facing per-phase history). */
|
|
118
|
+
readonly taskView?: TeamTaskViewLike;
|
|
119
|
+
}
|
|
120
|
+
/** Inputs for one audit-to-pass loop. */
|
|
121
|
+
export interface AuditToPassInput {
|
|
122
|
+
/** The agentTeams seam. */
|
|
123
|
+
readonly teams: TeamRuntimeLike;
|
|
124
|
+
/** Exact live Team member/lead authorizing the task mutations. */
|
|
125
|
+
readonly caller: TeamCallerHandle;
|
|
126
|
+
/** Phase + run identity (task subject/description vocabulary). */
|
|
127
|
+
readonly runId: string;
|
|
128
|
+
readonly phase: string;
|
|
129
|
+
/** Optional task blockers (previous-phase task ids). */
|
|
130
|
+
readonly blockedBy?: readonly TeamTaskIdLike[];
|
|
131
|
+
/** Write scopes for the phase artifact (advisory, overlap-warned). */
|
|
132
|
+
readonly writeScopes?: readonly string[];
|
|
133
|
+
/** Run ONE audit round for the current task; live usage delegates (T4). */
|
|
134
|
+
readonly runAuditRound: (round: number, task: TeamTaskViewLike) => Promise<AuditRoundOutcome>;
|
|
135
|
+
/** Lock the phase artifact — called ONLY after an APPROVE verdict. */
|
|
136
|
+
readonly lockPhase: () => Promise<void>;
|
|
137
|
+
/** Team member name to interrupt on a stuck reviewer (defaults to the role). */
|
|
138
|
+
readonly reviewerName?: string;
|
|
139
|
+
/** Round cap (fail loud past it; no lock). */
|
|
140
|
+
readonly maxRounds?: number;
|
|
141
|
+
/** Per-round wait timeout before the audit round runs (skipped without the seam). */
|
|
142
|
+
readonly waitTimeoutMs?: number;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Render a per-phase task history (board-facing; pure). One line per round plus
|
|
146
|
+
* the final task status — no live data, no mutation.
|
|
147
|
+
*/
|
|
148
|
+
export declare function renderTaskHistory(task: TeamTaskViewLike | undefined, rounds: readonly AuditLoopRound[]): string;
|
|
149
|
+
/**
|
|
150
|
+
* T3 driver: audit the phase until it passes, on ONE durable team task.
|
|
151
|
+
*
|
|
152
|
+
* Transition trail (the fake records exactly this order):
|
|
153
|
+
* createTask(pending) → claim(in_progress) → waitForChange → audit round
|
|
154
|
+
* → REVISE: updateTask(edit, repair) → waitForChange → re-audit SAME task
|
|
155
|
+
* → APPROVE: updateTask(complete) → lockPhase()
|
|
156
|
+
* → REJECT / cap / stuck: updateTask(release) + interrupt, NO lock.
|
|
157
|
+
*/
|
|
158
|
+
export declare function auditToPass(input: AuditToPassInput): Promise<AuditToPassResult>;
|
|
159
|
+
/** Whether a task view is currently claimed by the named owner (board-facing). */
|
|
160
|
+
export declare function isTaskClaimedBy(task: TeamTaskViewLike, ownerName: string): boolean;
|
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 + recursive_status tool",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -24,11 +24,12 @@ import { createHandoff, createChildBrief, replyPath, childScratchPath, buildDele
|
|
|
24
24
|
import { resolveRole, capabilityProbe, loadRouterPolicy } from '../src/router.ts'
|
|
25
25
|
import { validateReferences, writeActionRecord, reviewOutputSchema } from '../src/delegation.ts'
|
|
26
26
|
import { writeChildScratch, readParentScratch } from '../src/scratch.ts'
|
|
27
|
-
import {
|
|
27
|
+
import { validateTransition, coupleGateBlockToGoal, type PhaseTransitionIntent } from '../src/lifecycle.ts'
|
|
28
28
|
import { snapshotWorkspace } from '../src/snapshot.ts'
|
|
29
29
|
import { RECURSIVE_API_PREFIX, makeRecursiveRoutes, mountRecursiveRoutesOnce } from '../src/live-route.ts'
|
|
30
30
|
import { columnForRun, cardFacts, nodeKeyOf, expandPhaseRows } from '../src/client/derive.ts'
|
|
31
|
-
import {
|
|
31
|
+
import { evaluateToolGuard, coerceAskToDecision, resolveEnforcementConfig, DEFAULT_ENFORCEMENT, detectTamper } from '../src/enforcement.ts'
|
|
32
|
+
import { syncRunGoal, blockRunGoal, type GoalServiceLike, type GoalViewLike } from '../src/goals-projection.ts'
|
|
32
33
|
import { renderRecursivePolicy } from '../src/policy.ts'
|
|
33
34
|
|
|
34
35
|
let failures = 0
|
|
@@ -136,25 +137,35 @@ async function main() {
|
|
|
136
137
|
const arec = writeActionRecord({ root: wsA, runId: '10-smoke', subagentId: 'c1', phase: '03.5', purpose: 'review', executionMode: 'self-audit', success: false, stopReason: 'none' })
|
|
137
138
|
check('R6 action record', existsSync(arec) && readFileSync(arec, 'utf8').includes('## Verification Handoff'))
|
|
138
139
|
|
|
139
|
-
// Phase C (run 05) smoke:
|
|
140
|
-
|
|
141
|
-
{ type: 'recursive/phase', data: { runId: '10-smoke', phase: '03', status: 'LOCKED' } },
|
|
142
|
-
{ type: 'recursive/run-state', data: { runId: '10-smoke', state: 'active' } },
|
|
143
|
-
]
|
|
144
|
-
const folded = foldRecursivePhase(lifeEvents)
|
|
145
|
-
check('R1 fold last-wins phase', folded?.phase === '03' && folded?.runState === 'active')
|
|
140
|
+
// Phase C (run 05) smoke: transition gate (zero-emission surface), tool
|
|
141
|
+
// guard, T6 ask->policy bridge, policy, tamper, T1 goals projection.
|
|
146
142
|
const gatedIntent: PhaseTransitionIntent = { runId: '10-smoke', worktreeRoot: wsA, targetArtifact: '03-implementation-summary.md', kind: 'lock' }
|
|
147
|
-
const
|
|
148
|
-
check('
|
|
149
|
-
const gateStrict = evaluatePreStepGate([{ type: 'recursive/phase-intent', data: gatedIntent }], 'strict')
|
|
150
|
-
check('R3 pre-step strict rejects', gateStrict.kind === 'reject' && gateStrict.gateBlocked === true)
|
|
151
|
-
check('R3 pre-step no intent enters', evaluatePreStepGate([], 'strict').kind === 'enter')
|
|
143
|
+
const gateCheck = validateTransition(gatedIntent)
|
|
144
|
+
check('R1 transition gate reads the lock chain', gateCheck.passed === false && gateCheck.failures.length > 0, gateCheck.failures.join(';'))
|
|
152
145
|
const tddContent = 'Run: 10-smoke\nPhase: 3\nStatus: DRAFT\nTDD Mode: strict\nRED: evidence/logs/red/tdd-red.md\nGREEN: evidence/logs/green/tdd-green.md\n'
|
|
153
146
|
writeFileSync(join(runDir, '03-implementation-summary.md'), tddContent, 'utf8')
|
|
147
|
+
// Lock 03's prerequisites so the in-order lock guard passes (monotonic chain).
|
|
148
|
+
// 01/02 are audited phases: the fold requires Audit: PASS for lock-valid.
|
|
149
|
+
for (const [file, phase, audited] of [['00-worktree.md', '0 (Worktree)', false], ['01-as-is.md', '1 (AS-IS)', true], ['02-to-be-plan.md', '2 (TO-BE Plan)', true]] as const) {
|
|
150
|
+
const doc = [
|
|
151
|
+
'Run: 10-smoke', 'Phase: ' + phase, 'Status: `LOCKED`', 'Workflow version: recursive-mode-audit-v2', '',
|
|
152
|
+
'## TODO', '', '- [x] done', '', 'Coverage: PASS', 'Approval: PASS',
|
|
153
|
+
...(audited ? ['Audit: PASS'] : []),
|
|
154
|
+
'LockedAt: `2026-01-15T10:00:00Z`', 'LockHash: `PLACEHOLDER`', '',
|
|
155
|
+
].join('\n')
|
|
156
|
+
const docHash = lockHashFromContent(doc.replace('PLACEHOLDER', '0'.repeat(64)))
|
|
157
|
+
writeFileSync(join(runDir, file), doc.replace('PLACEHOLDER', docHash), 'utf8')
|
|
158
|
+
}
|
|
154
159
|
const lockGuard = evaluateToolGuard({ name: 'recursive_lock', arguments: { artifact: '03-implementation-summary.md' } }, wsA, '10-smoke', 'strict')
|
|
155
160
|
check('R4 tool guard allows in-order lock', lockGuard.kind === 'allow')
|
|
156
161
|
const outOfOrder = evaluateToolGuard({ name: 'recursive_lock', arguments: { artifact: '05-manual-qa.md' } }, wsA, '10-smoke', 'strict')
|
|
157
162
|
check('R4 tool guard denies out-of-order lock', outOfOrder.kind === 'deny' && (outOfOrder as { reason: string }).reason.includes('monotonic'))
|
|
163
|
+
const askDecision = evaluateToolGuard({ name: 'recursive_lock', arguments: { artifact: '05-manual-qa.md' } }, wsA, '10-smoke', 'advisory')
|
|
164
|
+
check('T6 advisory guard asks', askDecision.kind === 'ask')
|
|
165
|
+
const coercedStrict = coerceAskToDecision(askDecision, 'strict')
|
|
166
|
+
const coercedAdvisory = coerceAskToDecision(askDecision, 'advisory')
|
|
167
|
+
check('T6 ask->deny under strict', coercedStrict.kind === 'deny')
|
|
168
|
+
check('T6 ask->allow+warn under advisory (never silent)', coercedAdvisory.kind === 'allow' && typeof (coercedAdvisory as { warn?: string }).warn === 'string')
|
|
158
169
|
check('R7 config default advisory', JSON.stringify(resolveEnforcementConfig(undefined)) === JSON.stringify(DEFAULT_ENFORCEMENT))
|
|
159
170
|
let configError = ''
|
|
160
171
|
try { resolveEnforcementConfig({ bogus: 1 }) } catch (err) { configError = (err as Error).message }
|
|
@@ -163,6 +174,21 @@ async function main() {
|
|
|
163
174
|
check('R5 policy renders contract', policyText.includes('recursive-mode session') && policyText.includes('Current phase'))
|
|
164
175
|
check('R8 tamper clean', detectTamper(join(runDir, '03-implementation-summary.md'), wsA, '10-smoke') === null)
|
|
165
176
|
check('R6 goal coupling no-op', coupleGateBlockToGoal(null, {}, { id: 'g' }, { code: 'G', message: 'x' }) === false)
|
|
177
|
+
// T1 (goals projection): structural fake of the live goals service.
|
|
178
|
+
let goalCurrent: GoalViewLike | undefined
|
|
179
|
+
const goalService: GoalServiceLike = {
|
|
180
|
+
get: () => goalCurrent,
|
|
181
|
+
create: (_agent, req) => { goalCurrent = { id: 'g1', revision: 1, objective: req.objective, phase: 'active' }; return goalCurrent },
|
|
182
|
+
block: (_agent, ref) => { if (!goalCurrent || goalCurrent.id !== ref.id) throw new Error('mismatch'); goalCurrent = { ...goalCurrent, phase: 'blocked', revision: ref.revision + 1 }; return goalCurrent },
|
|
183
|
+
pause: (_agent, ref) => { goalCurrent = { ...(goalCurrent ?? { id: ref.id, revision: ref.revision }), phase: 'paused', revision: ref.revision + 1 }; return goalCurrent },
|
|
184
|
+
resume: (_agent, ref) => { goalCurrent = { ...(goalCurrent ?? { id: ref.id, revision: ref.revision }), phase: 'active', revision: ref.revision + 1 }; return goalCurrent },
|
|
185
|
+
complete: (_agent, ref) => { goalCurrent = { ...(goalCurrent ?? { id: ref.id, revision: ref.revision }), phase: 'complete', revision: ref.revision + 1 }; return goalCurrent },
|
|
186
|
+
clear: (_agent, ref) => { goalCurrent = undefined; return { id: ref.id, revision: ref.revision + 1 } },
|
|
187
|
+
}
|
|
188
|
+
const goalSync = syncRunGoal(goalService, {}, '10-smoke', 'active')
|
|
189
|
+
check('T1 run goal armed', goalSync.ok === true && goalCurrent?.objective === 'recursive-run:10-smoke · active')
|
|
190
|
+
const goalBlock = blockRunGoal(goalService, {}, '10-smoke', { code: 'prerequisite-blockers', message: 'monotonic lock-order' })
|
|
191
|
+
check('T1 gate-block blocks the run goal', goalBlock.ok === true && goalCurrent?.phase === 'blocked')
|
|
166
192
|
|
|
167
193
|
|
|
168
194
|
// ---- Phase D checks (SP2 R1 live route, replaces the session projection) ----
|
|
@@ -179,21 +205,21 @@ async function main() {
|
|
|
179
205
|
// R5/R6 derive over the snapshot card (furthest present phase is 06 -> closeout lane).
|
|
180
206
|
check('R5 columnForRun maps to the furthest present phase', columnForRun(card as never) === '6-8')
|
|
181
207
|
const facts = cardFacts(card as never)
|
|
182
|
-
//
|
|
183
|
-
// no gate-block (the fs carries
|
|
184
|
-
check('R5 cardFacts progress + tampered + gateBlocked', facts.lockedCount ===
|
|
208
|
+
// Locked groups: 00 (requirements+worktree share one group), 01, 02.
|
|
209
|
+
// 03 is DRAFT; no tamper; no gate-block (the fs carries no transient facts).
|
|
210
|
+
check('R5 cardFacts progress + tampered + gateBlocked', facts.lockedCount === 3 && facts.tampered === false && facts.gateBlocked === false)
|
|
185
211
|
// R7 node key
|
|
186
212
|
check('R7 nodeKeyOf includes worktreeRoot', nodeKeyOf('10-smoke', wsA).includes(wsA))
|
|
187
213
|
check('R6 expandPhaseRows always shows 3.5', expandPhaseRows(card as never).some(r => r.phase === '03.5'))
|
|
188
214
|
// R2 route: prefix + mountOnce-global (second mount no-ops)
|
|
189
215
|
check('R2 route prefix', RECURSIVE_API_PREFIX === '/.recursive/api')
|
|
190
216
|
const routes = makeRecursiveRoutes({ resolveRoot: async () => wsA, snapshot: async (root: string) => snapshotWorkspace(root), revision: () => 1 })
|
|
191
|
-
check('R2 route count', routes.length ===
|
|
217
|
+
check('R2 route count (state, events, doc)', routes.length === 3 && routes[0].kind === 'exact' && routes[1].kind === 'exact' && routes[2].kind === 'exact')
|
|
192
218
|
let registerCount = 0
|
|
193
219
|
const fakeServer = { register: () => { registerCount++; return () => {} } }
|
|
194
220
|
const mk = () => mountRecursiveRoutesOnce('@try-works/dsh-recursive-mode', () => makeRecursiveRoutes({ resolveRoot: async () => wsA, snapshot: async (root: string) => snapshotWorkspace(root), revision: () => 1 }), fakeServer)
|
|
195
221
|
mk(); mk()
|
|
196
|
-
check('R2 mountOnce-global (second mount no-ops)', registerCount ===
|
|
222
|
+
check('R2 mountOnce-global (second mount no-ops)', registerCount === 3)
|
|
197
223
|
|
|
198
224
|
console.log(failures === 0 ? 'SMOKE PASS' : 'SMOKE FAIL (' + failures + ' failures)')
|
|
199
225
|
process.exitCode = failures === 0 ? 0 : 1
|