@try-works/dsh-recursive-mode 0.1.18 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/git-context.d.ts +53 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +756 -26
- package/lib/init-templates.d.ts +4 -0
- package/lib/phase-rules.d.ts +28 -3
- package/lib/recursive_phase.tool.d.ts +9 -0
- package/lib/recursive_worktree.tool.d.ts +7 -0
- package/lib/runtime.d.ts +46 -1
- package/lib/ts-lint.d.ts +8 -0
- package/lib/worktree.d.ts +59 -0
- package/package.json +1 -1
- package/src/commands.ts +46 -0
- package/src/git-context.ts +132 -0
- package/src/index.ts +14 -3
- package/src/init-templates.ts +15 -2
- package/src/phase-rules.ts +49 -6
- package/src/policy.ts +1 -0
- package/src/recursive_init.tool.ts +9 -3
- package/src/recursive_phase.tool.ts +29 -0
- package/src/recursive_worktree.tool.ts +54 -0
- package/src/runtime.ts +75 -6
- package/src/ts-lint.ts +31 -0
- package/src/worktree.ts +229 -0
package/lib/index.js
CHANGED
|
@@ -6,6 +6,154 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
//#region src/git-context.ts
|
|
10
|
+
/**
|
|
11
|
+
* git-context.ts — live git facts used for worktree + branch awareness (R?).
|
|
12
|
+
*
|
|
13
|
+
* Worktree/branch awareness goal: the plugin must know WHICH checkout and
|
|
14
|
+
* WHICH branch a run is executing in, so Phase 0 records an honest base-vs-
|
|
15
|
+
* worktree branch split and lint fails when the recorded context no longer
|
|
16
|
+
* matches live git state.
|
|
17
|
+
*
|
|
18
|
+
* Detection primitives:
|
|
19
|
+
* - `isLinkedWorktree` — a directory is a linked git worktree when its private
|
|
20
|
+
* git dir (`git rev-parse --git-dir`) differs from the common git dir
|
|
21
|
+
* (`git rev-parse --git-common-dir`). The main checkout has both equal.
|
|
22
|
+
* - `upstreamBranch` — the current branch's upstream target with the remote
|
|
23
|
+
* prefix stripped (origin/main -> main). Used to infer the promotion source
|
|
24
|
+
* branch for dev/stage/main workflows.
|
|
25
|
+
*
|
|
26
|
+
* Every accessor is total: on missing git or non-git dirs it returns null /
|
|
27
|
+
* false, never throws. Callers defer rather than fail hard.
|
|
28
|
+
*/
|
|
29
|
+
/** Run git in a repo dir; return trimmed stdout or null on any failure. */
|
|
30
|
+
function gitRun(repoRoot, ...args) {
|
|
31
|
+
try {
|
|
32
|
+
return execFileSync("git", args, {
|
|
33
|
+
cwd: repoRoot,
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: [
|
|
36
|
+
"ignore",
|
|
37
|
+
"pipe",
|
|
38
|
+
"ignore"
|
|
39
|
+
]
|
|
40
|
+
}).trim();
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** True when a non-equal git-dir vs git-common-dir marks a linked worktree. */
|
|
46
|
+
function isLinkedWorktree(gitDir, commonDir) {
|
|
47
|
+
if (!gitDir || !commonDir) return false;
|
|
48
|
+
return gitDir !== commonDir;
|
|
49
|
+
}
|
|
50
|
+
/** Strip a remote ref prefix (origin/main -> main, remotes/origin/main -> main). */
|
|
51
|
+
function stripRemotePrefix(ref) {
|
|
52
|
+
if (!ref) return null;
|
|
53
|
+
const short = ref.trim().replace(/^refs\/remotes\//, "").replace(/^[^/]+\//, "");
|
|
54
|
+
return short === "" ? null : short;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Gather normalized git facts about `repoRoot`. Total: any git failure
|
|
58
|
+
* degrades to neutral values (false / null), never throws.
|
|
59
|
+
*/
|
|
60
|
+
function gitFacts(repoRoot) {
|
|
61
|
+
const gitDir = gitRun(repoRoot, "rev-parse", "--git-dir");
|
|
62
|
+
const commonDir = gitRun(repoRoot, "rev-parse", "--git-common-dir");
|
|
63
|
+
const headSha = gitRun(repoRoot, "rev-parse", "--verify", "HEAD^{commit}");
|
|
64
|
+
const branch = gitRun(repoRoot, "symbolic-ref", "--quiet", "--short", "HEAD");
|
|
65
|
+
const toplevel = gitRun(repoRoot, "rev-parse", "--show-toplevel");
|
|
66
|
+
const upstreamRef = gitRun(repoRoot, "rev-parse", "--abbrev-ref", "@{upstream}");
|
|
67
|
+
return {
|
|
68
|
+
headSha,
|
|
69
|
+
branch: branch || null,
|
|
70
|
+
detached: !branch,
|
|
71
|
+
isWorktree: isLinkedWorktree(gitDir, commonDir),
|
|
72
|
+
gitDir,
|
|
73
|
+
commonDir,
|
|
74
|
+
toplevel,
|
|
75
|
+
upstreamBranch: branch ? stripRemotePrefix(upstreamRef) : null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the effective base branch for a checkout. In a linked worktree this
|
|
80
|
+
* is the branch the work is based on — best inferred from the upstream target
|
|
81
|
+
* (dev/stage/main promotion source) when one is configured; otherwise fall
|
|
82
|
+
* back to the current branch. Returns null only when no branch exists.
|
|
83
|
+
*/
|
|
84
|
+
function resolveBaseBranch(facts) {
|
|
85
|
+
if (facts.branch && facts.upstreamBranch && facts.upstreamBranch !== facts.branch) return facts.upstreamBranch;
|
|
86
|
+
return facts.branch;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Verify a worktree branch is (still) based on the recorded base branch.
|
|
90
|
+
* Returns { ok, reason }. Fails when the base branch no longer exists, or the
|
|
91
|
+
* worktree branch no longer contains it (merge-base --is-ancestor fails).
|
|
92
|
+
* A missing/unresolvable branch degrades to ok:false with a reason; a
|
|
93
|
+
* non-git dir degrades to ok:true (defer — nothing to verify).
|
|
94
|
+
*/
|
|
95
|
+
function verifyBranchBase(repoRoot, baseBranch, worktreeBranch) {
|
|
96
|
+
if (!baseBranch || !worktreeBranch) return {
|
|
97
|
+
ok: true,
|
|
98
|
+
reason: null
|
|
99
|
+
};
|
|
100
|
+
if (!gitRun(repoRoot, "rev-parse", "--git-dir")) return {
|
|
101
|
+
ok: true,
|
|
102
|
+
reason: null
|
|
103
|
+
};
|
|
104
|
+
if (!gitRun(repoRoot, "rev-parse", "--verify", `${baseBranch}^{commit}`)) return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: `recorded base branch '${baseBranch}' does not resolve in this checkout`
|
|
107
|
+
};
|
|
108
|
+
if (!gitRun(repoRoot, "rev-parse", "--verify", `${worktreeBranch}^{commit}`)) return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: `recorded worktree branch '${worktreeBranch}' does not resolve in this checkout`
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
execFileSync("git", [
|
|
114
|
+
"-C",
|
|
115
|
+
repoRoot,
|
|
116
|
+
"merge-base",
|
|
117
|
+
"--is-ancestor",
|
|
118
|
+
baseBranch,
|
|
119
|
+
worktreeBranch
|
|
120
|
+
], { stdio: [
|
|
121
|
+
"ignore",
|
|
122
|
+
"pipe",
|
|
123
|
+
"pipe"
|
|
124
|
+
] });
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
reason: null
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: `worktree branch '${worktreeBranch}' is not based on recorded base branch '${baseBranch}'`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Verify the recorded worktree branch matches the live HEAD branch. */
|
|
137
|
+
function verifyWorktreeBranch(repoRoot, recordedBranch) {
|
|
138
|
+
if (!recordedBranch) return {
|
|
139
|
+
ok: true,
|
|
140
|
+
reason: null
|
|
141
|
+
};
|
|
142
|
+
const live = gitRun(repoRoot, "symbolic-ref", "--quiet", "--short", "HEAD");
|
|
143
|
+
if (!live) return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: "HEAD is detached; expected branch " + recordedBranch
|
|
146
|
+
};
|
|
147
|
+
if (live !== recordedBranch) return {
|
|
148
|
+
ok: false,
|
|
149
|
+
reason: `checkout is on branch '${live}' but 00-worktree.md records '${recordedBranch}'`
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
reason: null
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
9
157
|
//#region src/phase-rules.ts
|
|
10
158
|
/**
|
|
11
159
|
* Phase rules (R5): canonical parity port of lint-recursive-run.py's
|
|
@@ -245,17 +393,43 @@ function getArtifactRequiredSections(fileName, workflowProfile = CURRENT_WORKFLO
|
|
|
245
393
|
return headings;
|
|
246
394
|
}
|
|
247
395
|
/**
|
|
396
|
+
* LIVE BUG 6 (0.2.1): dedup gate for the agent/pre-step lint-rules reminder.
|
|
397
|
+
* Keyed by (root, runId, phase) so each new run or phase transition gets its own
|
|
398
|
+
* single injection, while re-arming steps in the SAME phase stay silent. Scoped
|
|
399
|
+
* to the apply() effect/fiber lifetime (one instance per mount), matching the
|
|
400
|
+
* repairedRoots dedup used by the scaffold repair above the injection point.
|
|
401
|
+
*/
|
|
402
|
+
var ReminderOnceGate = class {
|
|
403
|
+
seen = /* @__PURE__ */ new Set();
|
|
404
|
+
shouldInject(root, runId, phase) {
|
|
405
|
+
const key = root + "\0" + runId + "\0" + phase;
|
|
406
|
+
if (this.seen.has(key)) return false;
|
|
407
|
+
this.seen.add(key);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
function phaseRulesFor(fileName, workflowProfile = CURRENT_WORKFLOW_PROFILE$1) {
|
|
412
|
+
return {
|
|
413
|
+
fileName,
|
|
414
|
+
label: fileName.replace(/\.md$/, ""),
|
|
415
|
+
requiredSections: getArtifactRequiredSections(fileName, workflowProfile),
|
|
416
|
+
audited: AUDITED_PHASE_FILES$2.has(fileName),
|
|
417
|
+
tdd: fileName === "03-implementation-summary.md",
|
|
418
|
+
qa: fileName === "05-manual-qa.md"
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
248
422
|
* Compact lint-rules message for the pre-step injection (R5): this phase's
|
|
249
|
-
* required sections + phase-specific gate notes,
|
|
250
|
-
*
|
|
251
|
-
*
|
|
423
|
+
* required sections + phase-specific gate notes, built from phaseRulesFor
|
|
424
|
+
* (single source of truth with the recursive_phase tool). Output is unchanged
|
|
425
|
+
* from the prior inline build so r5-parity.spec.ts stays green.
|
|
252
426
|
*/
|
|
253
427
|
function phaseLintRulesMessage(fileName, workflowProfile = CURRENT_WORKFLOW_PROFILE$1) {
|
|
254
|
-
const
|
|
428
|
+
const rules = phaseRulesFor(fileName, workflowProfile);
|
|
255
429
|
return [
|
|
256
430
|
"<system-reminder>",
|
|
257
431
|
"Recursive-mode phase lint rules for THIS phase (" + fileName + "):",
|
|
258
|
-
"Required sections: " +
|
|
432
|
+
"Required sections: " + rules.requiredSections.join(" | "),
|
|
259
433
|
"Gates: Coverage: FAIL until all checkboxes pass; Approval: FAIL until user sign-off; lock only via recursive_lock (monotonic).",
|
|
260
434
|
"Audited phases: end with Audit: PASS before setting Coverage/Approval PASS; record Audit Context and Audit Verdict.",
|
|
261
435
|
"TDD (phase 3): declare TDD Mode: strict|pragmatic; strict requires RED + GREEN evidence paths.",
|
|
@@ -1048,6 +1222,24 @@ function getRunDiffBasis(runDir) {
|
|
|
1048
1222
|
notes: getMdFieldValue$1(source, "Diff basis notes") ?? getMdFieldValue$1(source, "Notes")
|
|
1049
1223
|
};
|
|
1050
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* verify_recorded_branches: worktree + branch awareness at lint time. Compares
|
|
1227
|
+
* the branches recorded in 00-worktree.md against live git state. Returns a
|
|
1228
|
+
* list of FAIL messages (empty when consistent). Both the base branch and the
|
|
1229
|
+
* worktree branch must resolve; the worktree branch must match the live HEAD
|
|
1230
|
+
* branch, and it must actually be based on the recorded base branch.
|
|
1231
|
+
*/
|
|
1232
|
+
function verifyRecordedBranches(repoRoot, diffBasis, runDir) {
|
|
1233
|
+
const fails = [];
|
|
1234
|
+
const baseBranch = trimMdValue$1(diffBasis.base_branch ?? "");
|
|
1235
|
+
const worktreeBranch = trimMdValue$1(diffBasis.worktree_branch ?? "");
|
|
1236
|
+
if (!baseBranch && !worktreeBranch) return fails;
|
|
1237
|
+
const wtCheck = verifyWorktreeBranch(repoRoot, worktreeBranch || null);
|
|
1238
|
+
if (!wtCheck.ok && wtCheck.reason) fails.push(wtCheck.reason);
|
|
1239
|
+
const baseCheck = verifyBranchBase(repoRoot, baseBranch || null, worktreeBranch || null);
|
|
1240
|
+
if (!baseCheck.ok && baseCheck.reason) fails.push(baseCheck.reason);
|
|
1241
|
+
return fails;
|
|
1242
|
+
}
|
|
1051
1243
|
/** normalize_diff_basis: validate + compute the executable diff basis. */
|
|
1052
1244
|
function normalizeDiffBasis(repoRoot, diffBasis) {
|
|
1053
1245
|
const baselineType = normalizeBaselineType(diffBasis.baseline_type);
|
|
@@ -2612,6 +2804,11 @@ function lintRun(repoRoot, runId) {
|
|
|
2612
2804
|
let diffBasisError = null;
|
|
2613
2805
|
if (STRICT_WORKFLOW_PROFILES$1.has(workflowProfile)) {
|
|
2614
2806
|
const diffBasis = getRunDiffBasis(runDir);
|
|
2807
|
+
const branchFails = verifyRecordedBranches(root, diffBasis, runDir);
|
|
2808
|
+
for (const message of branchFails) {
|
|
2809
|
+
totalFail += 1;
|
|
2810
|
+
writeIssue("FAIL", runDir, message);
|
|
2811
|
+
}
|
|
2615
2812
|
if (diffBasis.baseline_reference || diffBasis.normalized_baseline) {
|
|
2616
2813
|
const [rawChanged, gitError] = getGitChangedFiles(root, diffBasis);
|
|
2617
2814
|
if (gitError) {
|
|
@@ -2751,12 +2948,9 @@ function detectGitContext(repoRoot) {
|
|
|
2751
2948
|
context: {},
|
|
2752
2949
|
error: "Unable to resolve HEAD commit for Phase 0 diff basis prefill: git rev-parse returned no output"
|
|
2753
2950
|
};
|
|
2754
|
-
const
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
"--short",
|
|
2758
|
-
"HEAD"
|
|
2759
|
-
]) || "(detached HEAD)";
|
|
2951
|
+
const facts = gitFacts(repoRoot);
|
|
2952
|
+
const branch = facts.branch ?? "(detached HEAD)";
|
|
2953
|
+
const baseBranch = resolveBaseBranch(facts) ?? branch;
|
|
2760
2954
|
return {
|
|
2761
2955
|
context: {
|
|
2762
2956
|
baselineType: "local commit",
|
|
@@ -2765,9 +2959,11 @@ function detectGitContext(repoRoot) {
|
|
|
2765
2959
|
normalizedBaseline: headSha,
|
|
2766
2960
|
normalizedComparison: "working-tree",
|
|
2767
2961
|
normalizedDiffCommand: "git diff --name-only " + headSha,
|
|
2768
|
-
baseBranch
|
|
2962
|
+
baseBranch,
|
|
2769
2963
|
worktreeBranch: branch,
|
|
2770
2964
|
baseCommit: headSha,
|
|
2965
|
+
isWorktree: facts.isWorktree,
|
|
2966
|
+
upstreamBranch: facts.upstreamBranch,
|
|
2771
2967
|
notes: "recursive-init prefilled this executable diff basis from the current HEAD commit. If Phase 0 later changes the chosen baseline, update every diff-basis field and rerun lint before locking."
|
|
2772
2968
|
},
|
|
2773
2969
|
error: null
|
|
@@ -4671,7 +4867,8 @@ function renderRecursivePolicy(context) {
|
|
|
4671
4867
|
"- Phase 3 lock requires TDD evidence (strict) or rationale (pragmatic); Phase 5 requires QA sign-off for human/hybrid modes.",
|
|
4672
4868
|
"- The control-plane root is resolved STRICTLY from this session workspace (never scan other workspaces).",
|
|
4673
4869
|
"- Scratch is disposable, git-ignored, and never citable as an Input.",
|
|
4674
|
-
"- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing."
|
|
4870
|
+
"- The workflow spec lives at /.recursive/RECURSIVE.md; bootstrap it when missing.",
|
|
4871
|
+
"- Call recursive_phase when entering a new phase; the same rules are auto-injected once per phase transition."
|
|
4675
4872
|
];
|
|
4676
4873
|
if (currentFile) {
|
|
4677
4874
|
const sections = getArtifactRequiredSections(currentFile, CURRENT_WORKFLOW_PROFILE$1);
|
|
@@ -4687,6 +4884,272 @@ function renderRecursivePolicy(context) {
|
|
|
4687
4884
|
return lines.join("\n");
|
|
4688
4885
|
}
|
|
4689
4886
|
//#endregion
|
|
4887
|
+
//#region src/worktree.ts
|
|
4888
|
+
/**
|
|
4889
|
+
* worktree.ts — drive linked-worktree creation and branch promotion.
|
|
4890
|
+
*
|
|
4891
|
+
* Worktree + branch awareness, operational half (R?): after detection
|
|
4892
|
+
* (git-context.ts) and lint validation (ts-lint.ts), this module performs the
|
|
4893
|
+
* git operations that actually realize the dev/stage/main worktree workflow:
|
|
4894
|
+
*
|
|
4895
|
+
* - `createLinkedWorktree`: create a linked worktree at
|
|
4896
|
+
* `<repoRoot>/.worktrees/<runId>/` on a worktree branch based on a base
|
|
4897
|
+
* branch (default: the upstream/promotion source branch).
|
|
4898
|
+
* - `promoteBranch`: fast-forward one branch into a target promotion stage
|
|
4899
|
+
* (feature -> dev -> stage -> main). The promotion is verified to be a
|
|
4900
|
+
* fast-forward first (the target must be an ancestor of the source) and
|
|
4901
|
+
* then applied where it is safe:
|
|
4902
|
+
* - if the target branch is checked out in a worktree, `git merge
|
|
4903
|
+
* --ff-only` runs THERE (safe: moves the branch and its working tree
|
|
4904
|
+
* forward together, never a merge commit);
|
|
4905
|
+
* - otherwise the branch ref is updated directly.
|
|
4906
|
+
* It never rewrites history and never creates a merge commit.
|
|
4907
|
+
*
|
|
4908
|
+
* Both operations are explicit, workspace-scoped, and total: they return
|
|
4909
|
+
* { ok, ... } rather than corrupting git state. No operation ever touches a
|
|
4910
|
+
* branch other than the explicitly requested one.
|
|
4911
|
+
*/
|
|
4912
|
+
/** Run git; throw on failure (callers catch to turn into a result). */
|
|
4913
|
+
function gitThrow(repoRoot, ...args) {
|
|
4914
|
+
return execFileSync("git", args, {
|
|
4915
|
+
cwd: repoRoot,
|
|
4916
|
+
encoding: "utf8",
|
|
4917
|
+
stdio: [
|
|
4918
|
+
"ignore",
|
|
4919
|
+
"pipe",
|
|
4920
|
+
"pipe"
|
|
4921
|
+
]
|
|
4922
|
+
}).trim();
|
|
4923
|
+
}
|
|
4924
|
+
/** Resolve the current branch of a repo dir (short name or null when detached). */
|
|
4925
|
+
function currentBranch(repoRoot) {
|
|
4926
|
+
try {
|
|
4927
|
+
return gitThrow(repoRoot, "symbolic-ref", "--quiet", "--short", "HEAD");
|
|
4928
|
+
} catch {
|
|
4929
|
+
return null;
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
/** Default worktree branch name for a run. */
|
|
4933
|
+
function defaultWorktreeBranch(runId) {
|
|
4934
|
+
return "recursive/" + runId;
|
|
4935
|
+
}
|
|
4936
|
+
/** List linked worktrees (path + branch) for a repo root. */
|
|
4937
|
+
function listWorktrees(repoRoot) {
|
|
4938
|
+
let out;
|
|
4939
|
+
try {
|
|
4940
|
+
out = gitThrow(repoRoot, "worktree", "list", "--porcelain");
|
|
4941
|
+
} catch {
|
|
4942
|
+
return [];
|
|
4943
|
+
}
|
|
4944
|
+
const result = [];
|
|
4945
|
+
let current = null;
|
|
4946
|
+
for (const line of out.split(/\r?\n/)) {
|
|
4947
|
+
const trimmed = line.trim();
|
|
4948
|
+
if (!trimmed) {
|
|
4949
|
+
if (current) {
|
|
4950
|
+
result.push(current);
|
|
4951
|
+
current = null;
|
|
4952
|
+
}
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
if (trimmed.startsWith("worktree ")) current = {
|
|
4956
|
+
path: trimmed.slice(9),
|
|
4957
|
+
branch: null,
|
|
4958
|
+
detached: false
|
|
4959
|
+
};
|
|
4960
|
+
else if (trimmed.startsWith("branch refs/heads/") && current) current.branch = trimmed.slice(18);
|
|
4961
|
+
else if (trimmed === "detached" && current) current.detached = true;
|
|
4962
|
+
}
|
|
4963
|
+
if (current) result.push(current);
|
|
4964
|
+
return result;
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Find the worktree path where `branch` is currently checked out, or null.
|
|
4968
|
+
* Parses `git worktree list --porcelain` (block per worktree with a
|
|
4969
|
+
* `branch refs/heads/<name>` line).
|
|
4970
|
+
*/
|
|
4971
|
+
function findWorktreeForBranch(repoRoot, branch) {
|
|
4972
|
+
let out;
|
|
4973
|
+
try {
|
|
4974
|
+
out = gitThrow(repoRoot, "worktree", "list", "--porcelain");
|
|
4975
|
+
} catch {
|
|
4976
|
+
return null;
|
|
4977
|
+
}
|
|
4978
|
+
let currentPath = null;
|
|
4979
|
+
for (const line of out.split(/\r?\n/)) {
|
|
4980
|
+
const trimmed = line.trim();
|
|
4981
|
+
if (!trimmed) {
|
|
4982
|
+
currentPath = null;
|
|
4983
|
+
continue;
|
|
4984
|
+
}
|
|
4985
|
+
if (trimmed.startsWith("worktree ")) currentPath = trimmed.slice(9);
|
|
4986
|
+
else if (trimmed.startsWith("branch refs/heads/")) {
|
|
4987
|
+
if (trimmed.slice(18) === branch && currentPath) return currentPath;
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
return null;
|
|
4991
|
+
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Create a linked worktree at `.worktrees/<runId>/` for a run. The worktree
|
|
4994
|
+
* branch is cut from `baseBranch` (default: the current HEAD branch). Returns
|
|
4995
|
+
* the created worktree dir + branch. No-op-safe: if the worktree dir already
|
|
4996
|
+
* exists, returns ok with the existing dir. Refuses to create a worktree when
|
|
4997
|
+
* the run directory already exists (prevents clobbering an in-progress run).
|
|
4998
|
+
*/
|
|
4999
|
+
function createLinkedWorktree(opts) {
|
|
5000
|
+
const { repoRoot, runId } = opts;
|
|
5001
|
+
const baseBranch = opts.baseBranch ?? currentBranch(repoRoot) ?? "main";
|
|
5002
|
+
const worktreeBranch = opts.worktreeBranch ?? defaultWorktreeBranch(runId);
|
|
5003
|
+
const worktreeDir = join(repoRoot, ".worktrees", runId);
|
|
5004
|
+
if (existsSync(worktreeDir)) return {
|
|
5005
|
+
ok: true,
|
|
5006
|
+
worktreeDir,
|
|
5007
|
+
worktreeBranch,
|
|
5008
|
+
baseBranch,
|
|
5009
|
+
error: void 0
|
|
5010
|
+
};
|
|
5011
|
+
const runDir = join(repoRoot, ".recursive", "run", runId);
|
|
5012
|
+
if (existsSync(runDir)) return {
|
|
5013
|
+
ok: false,
|
|
5014
|
+
worktreeDir,
|
|
5015
|
+
worktreeBranch,
|
|
5016
|
+
baseBranch,
|
|
5017
|
+
error: "run directory already exists: " + runDir + " (refusing to create a worktree for an existing run)"
|
|
5018
|
+
};
|
|
5019
|
+
try {
|
|
5020
|
+
gitThrow(repoRoot, "rev-parse", "--verify", baseBranch + "^{commit}");
|
|
5021
|
+
} catch {
|
|
5022
|
+
return {
|
|
5023
|
+
ok: false,
|
|
5024
|
+
worktreeDir,
|
|
5025
|
+
worktreeBranch,
|
|
5026
|
+
baseBranch,
|
|
5027
|
+
error: "base branch does not resolve: " + baseBranch
|
|
5028
|
+
};
|
|
5029
|
+
}
|
|
5030
|
+
try {
|
|
5031
|
+
gitThrow(repoRoot, "worktree", "add", "-b", worktreeBranch, worktreeDir, baseBranch);
|
|
5032
|
+
} catch (err) {
|
|
5033
|
+
return {
|
|
5034
|
+
ok: false,
|
|
5035
|
+
worktreeDir,
|
|
5036
|
+
worktreeBranch,
|
|
5037
|
+
baseBranch,
|
|
5038
|
+
error: "git worktree add failed: " + err.message
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
return {
|
|
5042
|
+
ok: true,
|
|
5043
|
+
worktreeDir,
|
|
5044
|
+
worktreeBranch,
|
|
5045
|
+
baseBranch
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
/**
|
|
5049
|
+
* Fast-forward `toBranch` to `fromBranch` (promotion up the dev/stage/main
|
|
5050
|
+
* chain). Returns ok:false when the promotion is not a pure fast-forward
|
|
5051
|
+
* (would require a merge commit) or when the from/to branches are missing.
|
|
5052
|
+
*/
|
|
5053
|
+
function promoteBranch(opts) {
|
|
5054
|
+
const { repoRoot, fromBranch, toBranch } = opts;
|
|
5055
|
+
if (fromBranch === toBranch) return {
|
|
5056
|
+
ok: false,
|
|
5057
|
+
fromBranch,
|
|
5058
|
+
toBranch,
|
|
5059
|
+
action: "fast-forward",
|
|
5060
|
+
error: "from and to branches are identical: " + fromBranch
|
|
5061
|
+
};
|
|
5062
|
+
let fromSha = null;
|
|
5063
|
+
try {
|
|
5064
|
+
fromSha = gitThrow(repoRoot, "rev-parse", "--verify", fromBranch + "^{commit}");
|
|
5065
|
+
} catch {}
|
|
5066
|
+
if (!fromSha) return {
|
|
5067
|
+
ok: false,
|
|
5068
|
+
fromBranch,
|
|
5069
|
+
toBranch,
|
|
5070
|
+
action: "fast-forward",
|
|
5071
|
+
error: "from branch does not resolve: " + fromBranch
|
|
5072
|
+
};
|
|
5073
|
+
let toSha = null;
|
|
5074
|
+
try {
|
|
5075
|
+
toSha = gitThrow(repoRoot, "rev-parse", "--verify", toBranch + "^{commit}");
|
|
5076
|
+
} catch {}
|
|
5077
|
+
if (!toSha) try {
|
|
5078
|
+
gitThrow(repoRoot, "branch", toBranch, fromSha);
|
|
5079
|
+
return {
|
|
5080
|
+
ok: true,
|
|
5081
|
+
fromBranch,
|
|
5082
|
+
toBranch,
|
|
5083
|
+
action: "created"
|
|
5084
|
+
};
|
|
5085
|
+
} catch (err) {
|
|
5086
|
+
return {
|
|
5087
|
+
ok: false,
|
|
5088
|
+
fromBranch,
|
|
5089
|
+
toBranch,
|
|
5090
|
+
action: "created",
|
|
5091
|
+
error: "git branch failed: " + err.message
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5094
|
+
try {
|
|
5095
|
+
execFileSync("git", [
|
|
5096
|
+
"-C",
|
|
5097
|
+
repoRoot,
|
|
5098
|
+
"merge-base",
|
|
5099
|
+
"--is-ancestor",
|
|
5100
|
+
toBranch,
|
|
5101
|
+
fromBranch
|
|
5102
|
+
], { stdio: [
|
|
5103
|
+
"ignore",
|
|
5104
|
+
"pipe",
|
|
5105
|
+
"pipe"
|
|
5106
|
+
] });
|
|
5107
|
+
} catch {
|
|
5108
|
+
return {
|
|
5109
|
+
ok: false,
|
|
5110
|
+
fromBranch,
|
|
5111
|
+
toBranch,
|
|
5112
|
+
action: "fast-forward",
|
|
5113
|
+
error: "promotion is not a fast-forward: " + toBranch + " is not an ancestor of " + fromBranch
|
|
5114
|
+
};
|
|
5115
|
+
}
|
|
5116
|
+
const checkedOutIn = findWorktreeForBranch(repoRoot, toBranch);
|
|
5117
|
+
if (checkedOutIn) try {
|
|
5118
|
+
gitThrow(checkedOutIn, "merge", "--ff-only", fromBranch);
|
|
5119
|
+
return {
|
|
5120
|
+
ok: true,
|
|
5121
|
+
fromBranch,
|
|
5122
|
+
toBranch,
|
|
5123
|
+
action: "fast-forward"
|
|
5124
|
+
};
|
|
5125
|
+
} catch (err) {
|
|
5126
|
+
return {
|
|
5127
|
+
ok: false,
|
|
5128
|
+
fromBranch,
|
|
5129
|
+
toBranch,
|
|
5130
|
+
action: "fast-forward",
|
|
5131
|
+
error: "fast-forward merge in " + checkedOutIn + " failed: " + err.message
|
|
5132
|
+
};
|
|
5133
|
+
}
|
|
5134
|
+
try {
|
|
5135
|
+
gitThrow(repoRoot, "update-ref", "refs/heads/" + toBranch, fromSha);
|
|
5136
|
+
return {
|
|
5137
|
+
ok: true,
|
|
5138
|
+
fromBranch,
|
|
5139
|
+
toBranch,
|
|
5140
|
+
action: "fast-forward"
|
|
5141
|
+
};
|
|
5142
|
+
} catch (err) {
|
|
5143
|
+
return {
|
|
5144
|
+
ok: false,
|
|
5145
|
+
fromBranch,
|
|
5146
|
+
toBranch,
|
|
5147
|
+
action: "fast-forward",
|
|
5148
|
+
error: "git update-ref failed: " + err.message
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
//#endregion
|
|
4690
5153
|
//#region src/runtime.ts
|
|
4691
5154
|
var RecursiveRuntime = class extends Service {
|
|
4692
5155
|
/** Recursive-mode runtime service. Owns run-state reads + lock/init/lint operations. */
|
|
@@ -4947,17 +5410,54 @@ var RecursiveRuntime = class extends Service {
|
|
|
4947
5410
|
return foldRun(resolved.runDir, resolved.runId);
|
|
4948
5411
|
}
|
|
4949
5412
|
/**
|
|
5413
|
+
* LIVE BUG 6 refined: structured phase rules for the CURRENT phase. Resolves
|
|
5414
|
+
* the workspace root (same as status/lock), finds the latest run (or the
|
|
5415
|
+
* given runId), advances via getNextLegalPhase, and returns the phase's lint
|
|
5416
|
+
* rules + instructions. Returns null when no active phase exists. This is the
|
|
5417
|
+
* canonical data source for the recursive_phase tool.
|
|
5418
|
+
*/
|
|
5419
|
+
async phaseRules(runId, agent) {
|
|
5420
|
+
const root = await this.resolveRootFor(agent);
|
|
5421
|
+
if (!root) return null;
|
|
5422
|
+
const resolved = resolveRunDir(root, runId);
|
|
5423
|
+
if (!resolved) return null;
|
|
5424
|
+
const phase = getNextLegalPhase(resolved.runDir);
|
|
5425
|
+
if (!phase) return null;
|
|
5426
|
+
return {
|
|
5427
|
+
runId: resolved.runId,
|
|
5428
|
+
phase,
|
|
5429
|
+
...phaseRulesFor(phase)
|
|
5430
|
+
};
|
|
5431
|
+
}
|
|
5432
|
+
/**
|
|
4950
5433
|
* Scaffold a run directory with FULL per-phase templates (no-op if exists).
|
|
4951
5434
|
* 00-requirements.md + 00-worktree.md are byte-identical to canonical
|
|
4952
5435
|
* recursive-init.py (incl. git-context prefill); later phases carry every
|
|
4953
5436
|
* required section (get_artifact_required_sections) + TODO + FAIL gates.
|
|
4954
5437
|
* Also scaffolds addenda/subagents/router-prompts/evidence dirs. Returns the
|
|
4955
5438
|
* run dir + created artifacts.
|
|
5439
|
+
*
|
|
5440
|
+
* When `opts.createWorktree` is true, a linked worktree is first created at
|
|
5441
|
+
* `.worktrees/<runId>/` and the run is scaffolded INSIDE it (per the
|
|
5442
|
+
* "all subsequent phases execute in worktree context" rule). The worktree
|
|
5443
|
+
* branch defaults to `recursive/<runId>` and is cut from `opts.baseBranch`
|
|
5444
|
+
* (default: current HEAD branch of the root checkout).
|
|
4956
5445
|
*/
|
|
4957
|
-
async initRun(runId, agent) {
|
|
5446
|
+
async initRun(runId, agent, opts) {
|
|
4958
5447
|
const root = await this.resolveRootFor(agent);
|
|
4959
5448
|
if (!root) throw new Error("cannot resolve workspace control-plane root for this session");
|
|
4960
|
-
|
|
5449
|
+
let scaffoldRoot = root;
|
|
5450
|
+
let worktree;
|
|
5451
|
+
if (opts?.createWorktree) {
|
|
5452
|
+
worktree = createLinkedWorktree({
|
|
5453
|
+
repoRoot: root,
|
|
5454
|
+
runId,
|
|
5455
|
+
baseBranch: opts.baseBranch
|
|
5456
|
+
});
|
|
5457
|
+
if (!worktree.ok) throw new Error(worktree.error ?? "worktree create failed");
|
|
5458
|
+
scaffoldRoot = worktree.worktreeDir;
|
|
5459
|
+
}
|
|
5460
|
+
const runDir = join(scaffoldRoot, ".recursive", "run", runId);
|
|
4961
5461
|
mkdirSync(runDir, { recursive: true });
|
|
4962
5462
|
const created = [];
|
|
4963
5463
|
const existing = [];
|
|
@@ -4970,8 +5470,8 @@ var RecursiveRuntime = class extends Service {
|
|
|
4970
5470
|
mkdirSync(p, { recursive: true });
|
|
4971
5471
|
created.push(dir + "/");
|
|
4972
5472
|
}
|
|
4973
|
-
const { context: gitContext, error: prefillError } = detectGitContext(
|
|
4974
|
-
const phase0 = [["00-requirements.md", requirementsContent(runId, "feature", "")], ["00-worktree.md", worktreeContent(runId,
|
|
5473
|
+
const { context: gitContext, error: prefillError } = detectGitContext(scaffoldRoot);
|
|
5474
|
+
const phase0 = [["00-requirements.md", requirementsContent(runId, "feature", "")], ["00-worktree.md", worktreeContent(runId, scaffoldRoot, gitContext, prefillError)]];
|
|
4975
5475
|
for (const [file, content] of phase0) {
|
|
4976
5476
|
const path = join(runDir, file);
|
|
4977
5477
|
if (existsSync(path)) {
|
|
@@ -5001,12 +5501,52 @@ var RecursiveRuntime = class extends Service {
|
|
|
5001
5501
|
writeFileSync(path, laterPhaseContent(runId, file), "utf8");
|
|
5002
5502
|
created.push(file);
|
|
5003
5503
|
}
|
|
5004
|
-
|
|
5504
|
+
const result = {
|
|
5005
5505
|
runDir,
|
|
5006
5506
|
runId,
|
|
5007
5507
|
created,
|
|
5008
5508
|
existing
|
|
5009
5509
|
};
|
|
5510
|
+
if (worktree) result.worktree = worktree;
|
|
5511
|
+
return result;
|
|
5512
|
+
}
|
|
5513
|
+
/**
|
|
5514
|
+
* Create a linked worktree for a run under the given workspace root. The
|
|
5515
|
+
* worktree branch defaults to `recursive/<runId>` and is cut from the given
|
|
5516
|
+
* base branch (default: the current HEAD branch of the root checkout).
|
|
5517
|
+
* Refuses to create over an existing run directory. Workspace-scoped.
|
|
5518
|
+
*/
|
|
5519
|
+
createRunWorktree(root, runId, baseBranch) {
|
|
5520
|
+
return createLinkedWorktree({
|
|
5521
|
+
repoRoot: root,
|
|
5522
|
+
runId,
|
|
5523
|
+
baseBranch
|
|
5524
|
+
});
|
|
5525
|
+
}
|
|
5526
|
+
/**
|
|
5527
|
+
* Promote a branch up the dev/stage/main chain (fast-forward). Workspace-scoped.
|
|
5528
|
+
*/
|
|
5529
|
+
promoteRunBranch(root, fromBranch, toBranch) {
|
|
5530
|
+
return promoteBranch({
|
|
5531
|
+
repoRoot: root,
|
|
5532
|
+
fromBranch,
|
|
5533
|
+
toBranch
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
/**
|
|
5537
|
+
* Worktree + branch status for a workspace root: the linked worktrees,
|
|
5538
|
+
* which branch each is on, and the current checkout's base/upstream context.
|
|
5539
|
+
*/
|
|
5540
|
+
worktreeStatus(root) {
|
|
5541
|
+
const facts = gitFacts(root);
|
|
5542
|
+
const worktrees = listWorktrees(root);
|
|
5543
|
+
return {
|
|
5544
|
+
root,
|
|
5545
|
+
isWorktree: facts.isWorktree,
|
|
5546
|
+
branch: facts.branch,
|
|
5547
|
+
upstreamBranch: facts.upstreamBranch,
|
|
5548
|
+
worktrees
|
|
5549
|
+
};
|
|
5010
5550
|
}
|
|
5011
5551
|
/**
|
|
5012
5552
|
* Lock a DRAFT artifact (or reopen a LOCKED one). Validates prerequisites;
|
|
@@ -5232,11 +5772,21 @@ function createRecursiveStatusTool(recursive) {
|
|
|
5232
5772
|
function createRecursiveInitTool(recursive) {
|
|
5233
5773
|
return defineTool({
|
|
5234
5774
|
name: "recursive_init",
|
|
5235
|
-
description: "Scaffold a new recursive-mode run directory (or ensure an existing one) with stub artifact headers. Delegates to the RecursiveRuntime service (no duplicated scaffolding logic).",
|
|
5236
|
-
parameters: {
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5775
|
+
description: "Scaffold a new recursive-mode run directory (or ensure an existing one) with stub artifact headers. Delegates to the RecursiveRuntime service (no duplicated scaffolding logic). When createWorktree is true, a linked worktree is created first and the run is scaffolded inside it.",
|
|
5776
|
+
parameters: {
|
|
5777
|
+
runId: {
|
|
5778
|
+
type: "string",
|
|
5779
|
+
description: "Run id (e.g. 03-something). Required."
|
|
5780
|
+
},
|
|
5781
|
+
createWorktree: {
|
|
5782
|
+
type: "boolean",
|
|
5783
|
+
description: "If true, create a linked worktree at .worktrees/<runId>/ and scaffold the run inside it (default: false)."
|
|
5784
|
+
},
|
|
5785
|
+
baseBranch: {
|
|
5786
|
+
type: "string",
|
|
5787
|
+
description: "Base branch the worktree branch is cut from (default: current HEAD branch). Only used when createWorktree is true."
|
|
5788
|
+
}
|
|
5789
|
+
},
|
|
5240
5790
|
output: {
|
|
5241
5791
|
schema: { type: "json" },
|
|
5242
5792
|
render: (_args, value) => [{
|
|
@@ -5247,7 +5797,10 @@ function createRecursiveInitTool(recursive) {
|
|
|
5247
5797
|
async execute(args, exec) {
|
|
5248
5798
|
if (!args.runId || args.runId.trim() === "") return { error: "runId is required" };
|
|
5249
5799
|
try {
|
|
5250
|
-
return await recursive.initRun(args.runId.trim(), exec.agent
|
|
5800
|
+
return await recursive.initRun(args.runId.trim(), exec.agent, {
|
|
5801
|
+
createWorktree: args.createWorktree === true,
|
|
5802
|
+
baseBranch: args.baseBranch?.trim() || void 0
|
|
5803
|
+
});
|
|
5251
5804
|
} catch (err) {
|
|
5252
5805
|
return { error: err instanceof Error ? err.message : String(err) };
|
|
5253
5806
|
}
|
|
@@ -5411,6 +5964,94 @@ function createRecursiveScratchTool(recursive) {
|
|
|
5411
5964
|
});
|
|
5412
5965
|
}
|
|
5413
5966
|
//#endregion
|
|
5967
|
+
//#region src/recursive_worktree.tool.ts
|
|
5968
|
+
/**
|
|
5969
|
+
* `recursive_worktree` — create a linked git worktree for a run and/or
|
|
5970
|
+
* promote a branch up the dev/stage/main chain. Workspace-scoped: the
|
|
5971
|
+
* operations run under the SESSION's control-plane root only.
|
|
5972
|
+
*/
|
|
5973
|
+
function createRecursiveWorktreeTool(recursive) {
|
|
5974
|
+
return defineTool({
|
|
5975
|
+
name: "recursive_worktree",
|
|
5976
|
+
description: "Create a linked git worktree for a recursive-mode run and/or promote a branch up the dev/stage/main chain. Workspace-scoped under the current session workspace.",
|
|
5977
|
+
parameters: {
|
|
5978
|
+
runId: {
|
|
5979
|
+
type: "string",
|
|
5980
|
+
description: "Run id the worktree is created for (e.g. 03-something). Required for create."
|
|
5981
|
+
},
|
|
5982
|
+
action: {
|
|
5983
|
+
type: "string",
|
|
5984
|
+
description: "create | promote | status. Default: create."
|
|
5985
|
+
},
|
|
5986
|
+
fromBranch: {
|
|
5987
|
+
type: "string",
|
|
5988
|
+
description: "Source branch for a promote action (the branch holding the new commits)."
|
|
5989
|
+
},
|
|
5990
|
+
toBranch: {
|
|
5991
|
+
type: "string",
|
|
5992
|
+
description: "Promotion target branch for a promote action (feature -> dev -> stage -> main)."
|
|
5993
|
+
},
|
|
5994
|
+
baseBranch: {
|
|
5995
|
+
type: "string",
|
|
5996
|
+
description: "Base branch the worktree branch is cut from (default: current HEAD branch)."
|
|
5997
|
+
}
|
|
5998
|
+
},
|
|
5999
|
+
output: {
|
|
6000
|
+
schema: { type: "json" },
|
|
6001
|
+
render: (_args, value) => [{
|
|
6002
|
+
type: "text",
|
|
6003
|
+
text: JSON.stringify(value, null, 2)
|
|
6004
|
+
}]
|
|
6005
|
+
},
|
|
6006
|
+
async execute(args, exec) {
|
|
6007
|
+
const action = args.action ?? "create";
|
|
6008
|
+
const root = await recursive.resolveRootFor(exec.agent);
|
|
6009
|
+
if (!root) return { error: "session is not attached to a registered workspace (cannot resolve control-plane root)" };
|
|
6010
|
+
if (action === "create") {
|
|
6011
|
+
if (!args.runId || args.runId.trim() === "") return { error: "runId is required for create" };
|
|
6012
|
+
return recursive.createRunWorktree(root, args.runId.trim(), args.baseBranch?.trim() || void 0);
|
|
6013
|
+
}
|
|
6014
|
+
if (action === "promote") {
|
|
6015
|
+
if (!args.fromBranch || !args.toBranch) return { error: "fromBranch and toBranch are required for promote" };
|
|
6016
|
+
return recursive.promoteRunBranch(root, args.fromBranch.trim(), args.toBranch.trim());
|
|
6017
|
+
}
|
|
6018
|
+
if (action === "status") return recursive.worktreeStatus(root);
|
|
6019
|
+
return { error: "action must be create | promote | status" };
|
|
6020
|
+
}
|
|
6021
|
+
});
|
|
6022
|
+
}
|
|
6023
|
+
//#endregion
|
|
6024
|
+
//#region src/recursive_phase.tool.ts
|
|
6025
|
+
/**
|
|
6026
|
+
* recursive_phase (refined LIVE BUG 6): the canonical on-demand home of the
|
|
6027
|
+
* current phase's lint rules + instructions. Reads the same structured source
|
|
6028
|
+
* (runtime.phaseRules -> phaseRulesFor) as the once-per-phase pre-step
|
|
6029
|
+
* reminder, so the agent can re-ask for the rules without re-injecting them on
|
|
6030
|
+
* every step. Returns { error } when no active phase is found.
|
|
6031
|
+
*/
|
|
6032
|
+
function createRecursivePhaseTool(recursive) {
|
|
6033
|
+
return defineTool({
|
|
6034
|
+
name: "recursive_phase",
|
|
6035
|
+
description: "Return the lint rules + instructions for the current recursive-mode phase (required sections, gates, TDD/QA notes). Call once when entering a new phase; the same rules are also auto-injected once per phase transition.",
|
|
6036
|
+
parameters: { runId: {
|
|
6037
|
+
type: "string",
|
|
6038
|
+
description: "Optional run id (defaults to the latest run by mtime)"
|
|
6039
|
+
} },
|
|
6040
|
+
output: {
|
|
6041
|
+
schema: { type: "json" },
|
|
6042
|
+
render: (_args, value) => [{
|
|
6043
|
+
type: "text",
|
|
6044
|
+
text: JSON.stringify(value, null, 2)
|
|
6045
|
+
}]
|
|
6046
|
+
},
|
|
6047
|
+
async execute(args, exec) {
|
|
6048
|
+
const result = await recursive.phaseRules(args.runId, exec.agent);
|
|
6049
|
+
if (!result) return { error: "no recursive phase found" };
|
|
6050
|
+
return result;
|
|
6051
|
+
}
|
|
6052
|
+
});
|
|
6053
|
+
}
|
|
6054
|
+
//#endregion
|
|
5414
6055
|
//#region src/bootstrap.ts
|
|
5415
6056
|
/**
|
|
5416
6057
|
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
@@ -5851,6 +6492,46 @@ function executeRecursiveCommand(root, rawInput) {
|
|
|
5851
6492
|
text: "closeout scaffolded for " + runId + " phase " + phase
|
|
5852
6493
|
};
|
|
5853
6494
|
}
|
|
6495
|
+
case "worktree": {
|
|
6496
|
+
const parts = arg.trim().split(/\s+/);
|
|
6497
|
+
const op = parts[0] ?? "";
|
|
6498
|
+
if (op === "status") return {
|
|
6499
|
+
kind: "success",
|
|
6500
|
+
text: "worktree status for workspace " + root
|
|
6501
|
+
};
|
|
6502
|
+
if (op === "create") {
|
|
6503
|
+
const runId = parts[1] ?? "";
|
|
6504
|
+
if (!runId) return {
|
|
6505
|
+
kind: "error",
|
|
6506
|
+
text: "worktree create requires a run id"
|
|
6507
|
+
};
|
|
6508
|
+
const runDir = join(runRoot, runId);
|
|
6509
|
+
if (existsSync(runDir)) return {
|
|
6510
|
+
kind: "error",
|
|
6511
|
+
text: "Run already exists in this workspace: " + runId
|
|
6512
|
+
};
|
|
6513
|
+
return {
|
|
6514
|
+
kind: "success",
|
|
6515
|
+
text: "worktree created for run " + runId + " at .worktrees/" + runId
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
6518
|
+
if (op === "promote") {
|
|
6519
|
+
const fromBranch = parts[1] ?? "";
|
|
6520
|
+
const toBranch = parts[2] ?? "";
|
|
6521
|
+
if (!fromBranch || !toBranch) return {
|
|
6522
|
+
kind: "error",
|
|
6523
|
+
text: "worktree promote requires <from> <to>"
|
|
6524
|
+
};
|
|
6525
|
+
return {
|
|
6526
|
+
kind: "success",
|
|
6527
|
+
text: "promoted " + fromBranch + " -> " + toBranch
|
|
6528
|
+
};
|
|
6529
|
+
}
|
|
6530
|
+
return {
|
|
6531
|
+
kind: "error",
|
|
6532
|
+
text: "worktree requires create|promote|status"
|
|
6533
|
+
};
|
|
6534
|
+
}
|
|
5854
6535
|
case "scratch": {
|
|
5855
6536
|
const runId = arg.trim();
|
|
5856
6537
|
if (!runId) return {
|
|
@@ -5916,6 +6597,48 @@ function registerRecursiveCommand(ctx, recursive) {
|
|
|
5916
6597
|
};
|
|
5917
6598
|
}
|
|
5918
6599
|
}
|
|
6600
|
+
if (verb === "worktree" && arg) {
|
|
6601
|
+
const parts = arg.trim().split(/\s+/);
|
|
6602
|
+
const op = parts[0] ?? "";
|
|
6603
|
+
if (op === "create") {
|
|
6604
|
+
const runId = parts[1] ?? "";
|
|
6605
|
+
const baseBranch = arg.match(/--base\s+(\S+)/)?.[1];
|
|
6606
|
+
if (!runId) return {
|
|
6607
|
+
kind: "error",
|
|
6608
|
+
text: "worktree create requires a run id"
|
|
6609
|
+
};
|
|
6610
|
+
const result = recursive.createRunWorktree(root, runId, baseBranch);
|
|
6611
|
+
if (!result.ok) return {
|
|
6612
|
+
kind: "error",
|
|
6613
|
+
text: result.error ?? "worktree create failed"
|
|
6614
|
+
};
|
|
6615
|
+
return {
|
|
6616
|
+
kind: "success",
|
|
6617
|
+
text: "worktree created: " + JSON.stringify(result)
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
if (op === "promote") {
|
|
6621
|
+
const fromBranch = parts[1] ?? "";
|
|
6622
|
+
const toBranch = parts[2] ?? "";
|
|
6623
|
+
if (!fromBranch || !toBranch) return {
|
|
6624
|
+
kind: "error",
|
|
6625
|
+
text: "worktree promote requires <from> <to>"
|
|
6626
|
+
};
|
|
6627
|
+
const result = recursive.promoteRunBranch(root, fromBranch, toBranch);
|
|
6628
|
+
if (!result.ok) return {
|
|
6629
|
+
kind: "error",
|
|
6630
|
+
text: result.error ?? "promote failed"
|
|
6631
|
+
};
|
|
6632
|
+
return {
|
|
6633
|
+
kind: "success",
|
|
6634
|
+
text: "promoted: " + JSON.stringify(result)
|
|
6635
|
+
};
|
|
6636
|
+
}
|
|
6637
|
+
if (op === "status") return {
|
|
6638
|
+
kind: "success",
|
|
6639
|
+
text: "worktree status: " + JSON.stringify(recursive.worktreeStatus(root))
|
|
6640
|
+
};
|
|
6641
|
+
}
|
|
5919
6642
|
return executeRecursiveCommand(root, rawInput);
|
|
5920
6643
|
}
|
|
5921
6644
|
});
|
|
@@ -6200,13 +6923,16 @@ function apply(ctx, config) {
|
|
|
6200
6923
|
workspaceRegistry
|
|
6201
6924
|
});
|
|
6202
6925
|
const repairedRoots = /* @__PURE__ */ new Set();
|
|
6926
|
+
const reminderGate = new ReminderOnceGate();
|
|
6203
6927
|
const disposers = [
|
|
6204
6928
|
ctx.tools.register(createRecursiveStatusTool(recursive)),
|
|
6205
6929
|
ctx.tools.register(createRecursiveInitTool(recursive)),
|
|
6206
6930
|
ctx.tools.register(createRecursiveLockTool(recursive)),
|
|
6207
6931
|
ctx.tools.register(createRecursiveLintTool(recursive)),
|
|
6208
6932
|
ctx.tools.register(createRecursiveCloseoutTool(recursive)),
|
|
6209
|
-
ctx.tools.register(createRecursiveScratchTool(recursive))
|
|
6933
|
+
ctx.tools.register(createRecursiveScratchTool(recursive)),
|
|
6934
|
+
ctx.tools.register(createRecursiveWorktreeTool(recursive)),
|
|
6935
|
+
ctx.tools.register(createRecursivePhaseTool(recursive))
|
|
6210
6936
|
];
|
|
6211
6937
|
const commands = ctx.get("commands");
|
|
6212
6938
|
if (commands) disposers.push(registerRecursiveCommand({ commands }, recursive));
|
|
@@ -6277,6 +7003,10 @@ function apply(ctx, config) {
|
|
|
6277
7003
|
kind: "enter",
|
|
6278
7004
|
messages
|
|
6279
7005
|
};
|
|
7006
|
+
if (!reminderGate.shouldInject(root, runId, phase)) return {
|
|
7007
|
+
kind: "enter",
|
|
7008
|
+
messages
|
|
7009
|
+
};
|
|
6280
7010
|
const reminder = phaseLintRulesMessage(phase);
|
|
6281
7011
|
return {
|
|
6282
7012
|
kind: "enter",
|
|
@@ -6308,4 +7038,4 @@ function apply(ctx, config) {
|
|
|
6308
7038
|
});
|
|
6309
7039
|
}
|
|
6310
7040
|
//#endregion
|
|
6311
|
-
export { DEFAULT_ENFORCEMENT, LifecycleDriver, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursiveScratchTool, createRecursiveStatusTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, detectTransitionIntent, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluatePreStepGate, evaluateToolGuard, foldRecursivePhase, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, hasOpenTurn, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|
|
7041
|
+
export { DEFAULT_ENFORCEMENT, LifecycleDriver, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, detectTransitionIntent, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluatePreStepGate, evaluateToolGuard, foldRecursivePhase, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, hasOpenTurn, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|