@infinitedusky/indusk-mcp 1.44.0 → 1.44.2
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/admin/.next/BUILD_ID +1 -1
- package/admin/.next/build-manifest.json +3 -3
- package/admin/.next/fallback-build-manifest.json +3 -3
- package/admin/.next/prerender-manifest.json +3 -3
- package/admin/.next/server/app/_global-error.html +1 -1
- package/admin/.next/server/app/_global-error.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/admin/.next/server/middleware-build-manifest.js +3 -3
- package/admin/.next/server/pages/500.html +1 -1
- package/admin/.next/server/server-reference-manifest.js +1 -1
- package/admin/.next/server/server-reference-manifest.json +1 -1
- package/admin/.next/trace +1 -1
- package/admin/.next/trace-build +1 -1
- package/dist/bin/commands/agent.js +3 -3
- package/dist/bin/commands/extensions.d.ts +11 -0
- package/dist/bin/commands/extensions.js +10 -0
- package/dist/bin/commands/run.js +21 -0
- package/dist/bin/commands/update.js +18 -14
- package/dist/bin/commands/workbench.d.ts +0 -8
- package/dist/bin/commands/workbench.js +28 -10
- package/dist/bin/commands/worktree.d.ts +10 -0
- package/dist/bin/commands/worktree.js +12 -5
- package/dist/lib/cleanup/oversized.js +16 -4
- package/dist/lib/eval/evaluator-runner.d.ts +3 -2
- package/dist/lib/stray-state-audit.js +5 -2
- package/dist/lib/trajectory/state-ops.d.ts +0 -6
- package/dist/lib/trajectory/state-ops.js +0 -15
- package/dist/lib/verify/roots.js +5 -2
- package/dist/lib/worktree/layout.js +9 -3
- package/dist/lib/worktree/repos.d.ts +10 -1
- package/dist/lib/worktree/repos.js +13 -2
- package/dist/lib/worktree/shareable.d.ts +3 -1
- package/dist/lib/worktree/shareable.js +53 -5
- package/extensions/worktree/hooks/on_enable.sh +17 -4
- package/extensions/worktree/manifest.json +1 -0
- package/extensions/worktree/scripts/lib/workbench-helpers.sh +58 -4
- package/extensions/worktree/scripts/preflight.sh +10 -46
- package/extensions/worktree/scripts/refresh-worktree.sh +18 -10
- package/extensions/worktree/scripts/setup-worktree.sh +11 -3
- package/extensions/worktree/templates/worktree-config.template.json +1 -1
- package/hooks/_hook-paths.js +129 -17
- package/hooks/_impl-phases.js +88 -0
- package/hooks/check-gates.js +3 -70
- package/hooks/eval-trigger.js +31 -5
- package/hooks/gate-reminder.js +92 -187
- package/package.json +2 -2
- package/skills/cleanup.md +1 -1
- package/skills/retrospective.md +1 -0
- /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_buildManifest.js +0 -0
- /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_clientMiddlewareManifest.js +0 -0
- /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { isUsableRelPath } from "../../lib/path-segment.js";
|
|
6
6
|
import { isDanglingLink, linkTrunk, listWorkbenchSubdirs, worktreeOwner, } from "../../lib/worktree/layout.js";
|
|
7
7
|
import { isWorkbench, readReposRoot, readWorkbenchRepos, repoDir, } from "../../lib/worktree/repos.js";
|
|
@@ -92,15 +92,19 @@ function refuseIfIgnoreCannotHold(projectRoot, repos, skip) {
|
|
|
92
92
|
if (skip)
|
|
93
93
|
return;
|
|
94
94
|
// A declared layout names its worktree directories exactly, so no
|
|
95
|
-
// deny-by-default rule is required and nothing to
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// deny-by-default rule is required and there is nothing to REFUSE. It
|
|
96
|
+
// still needs the machine-local rules, so the top-up runs first — before
|
|
97
|
+
// the early return, which used to strand every declared workbench without
|
|
98
|
+
// them.
|
|
99
|
+
const layoutDeclared = allLocationsDeclared(repos);
|
|
98
100
|
// InDusk wrote this file, so InDusk may extend it. Every freshly created
|
|
99
101
|
// workbench lands here — `init` scaffolds a `.gitignore`, so without this
|
|
100
102
|
// the refusal fires on the product's own output.
|
|
101
|
-
if (topUpManagedIgnore(projectRoot)) {
|
|
102
|
-
console.info("Added the
|
|
103
|
+
if (topUpManagedIgnore(projectRoot, { layoutDeclared })) {
|
|
104
|
+
console.info("Added the missing InDusk rules to the managed .gitignore.");
|
|
103
105
|
}
|
|
106
|
+
if (layoutDeclared)
|
|
107
|
+
return;
|
|
104
108
|
const gaps = missingIgnoreRules(projectRoot);
|
|
105
109
|
if (gaps.length === 0)
|
|
106
110
|
return;
|
|
@@ -126,6 +130,16 @@ function refuseIfIgnoreCannotHold(projectRoot, repos, skip) {
|
|
|
126
130
|
* it. They are two jobs, and the four-way ternary that used to sit inline made
|
|
127
131
|
* that hard to see.
|
|
128
132
|
*/
|
|
133
|
+
/**
|
|
134
|
+
* Where restore materializes a repo: at its DECLARED location — `path`, else
|
|
135
|
+
* name — which is where health, status, doppler and the update nudge all look.
|
|
136
|
+
* Cloning at the name put a second copy beside a repo declared at a `path` and
|
|
137
|
+
* linked the trunk at the wrong one (workbench-trust-fixes, F4). One function so
|
|
138
|
+
* the clone and the line that reports it cannot name different places.
|
|
139
|
+
*/
|
|
140
|
+
function cloneTarget(repo, siblingParent) {
|
|
141
|
+
return join(siblingParent, repoDir(repo));
|
|
142
|
+
}
|
|
129
143
|
export function restoreLine(repo, status, siblingParent) {
|
|
130
144
|
// `linkTrunk` deliberately refuses to replace a real directory sitting at
|
|
131
145
|
// the trunk path, so both `-unlinked` cases are correct behavior that the
|
|
@@ -138,10 +152,12 @@ export function restoreLine(repo, status, siblingParent) {
|
|
|
138
152
|
return `${repo.name} — present in the workbench at ${repoDir(repo)}/`;
|
|
139
153
|
case "nested-cloned":
|
|
140
154
|
return `${repo.name} — cloned into the workbench at ${repoDir(repo)}/`;
|
|
155
|
+
// The path printed is the path used — `restoreOne` clones at the
|
|
156
|
+
// declared location, so this must name it, not the sibling parent.
|
|
141
157
|
case "cloned":
|
|
142
|
-
return `${repo.name} — cloned into ${siblingParent} and linked`;
|
|
158
|
+
return `${repo.name} — cloned into ${cloneTarget(repo, siblingParent)} and linked`;
|
|
143
159
|
case "cloned-unlinked":
|
|
144
|
-
return `${repo.name} — cloned into ${siblingParent}
|
|
160
|
+
return `${repo.name} — cloned into ${cloneTarget(repo, siblingParent)}, but ${unlinked}`;
|
|
145
161
|
case "present-unlinked":
|
|
146
162
|
return `${repo.name} — already present, but ${unlinked}`;
|
|
147
163
|
case "present":
|
|
@@ -149,7 +165,7 @@ export function restoreLine(repo, status, siblingParent) {
|
|
|
149
165
|
}
|
|
150
166
|
}
|
|
151
167
|
function restoreOne(repo, workbenchRoot, siblingParent) {
|
|
152
|
-
const target =
|
|
168
|
+
const target = cloneTarget(repo, siblingParent);
|
|
153
169
|
// When the repo lives INSIDE the workbench at its trunk path — the nested
|
|
154
170
|
// layout `repos_root: "."` exists to express — there is no link to make and
|
|
155
171
|
// nothing wrong. Reporting that as "a real directory occupies …" describes
|
|
@@ -180,7 +196,9 @@ function restoreOne(repo, workbenchRoot, siblingParent) {
|
|
|
180
196
|
// nowhere is not something anyone loses.
|
|
181
197
|
if (isDanglingLink(target))
|
|
182
198
|
rmSync(target);
|
|
183
|
-
|
|
199
|
+
// The declared path may have its own parent (`code/alpha`); make it, not
|
|
200
|
+
// just the sibling parent, or the clone fails on a directory nothing made.
|
|
201
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
184
202
|
const { ok, stderr } = git(["clone", "--quiet", repo.remote, target], siblingParent);
|
|
185
203
|
if (!ok) {
|
|
186
204
|
return {
|
|
@@ -42,6 +42,16 @@ export declare function resolveCreateTarget(repos: readonly {
|
|
|
42
42
|
baseBranch?: string;
|
|
43
43
|
};
|
|
44
44
|
export declare function worktreeCreate(args: string[], projectRoot?: string): never;
|
|
45
|
+
/**
|
|
46
|
+
* Read `post_create` commands from the worktree config of THE repo being
|
|
47
|
+
* created — run in each new worktree after create.
|
|
48
|
+
*
|
|
49
|
+
* It read `repos[0]`'s config for every repo, so in a multi-repo workbench
|
|
50
|
+
* `worktree create beta <slug>` ran alpha's install steps in beta's worktree
|
|
51
|
+
* (workbench-trust-fixes, F6 / A15). The caller names the repo; with none
|
|
52
|
+
* named the bash script has already refused, so there is nothing to run.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readPostCreate(workbenchRoot: string, repo: string | undefined): string[];
|
|
45
55
|
export declare function worktreeRefresh(slug: string): never;
|
|
46
56
|
export declare function worktreePreflight(slug: string, baseBranch?: string): never;
|
|
47
57
|
/**
|
|
@@ -175,7 +175,7 @@ function worktreeCreateResolved(slug, baseBranch, repo) {
|
|
|
175
175
|
if (provisionWorktreeEnv(workbenchRoot, worktreeDir)) {
|
|
176
176
|
console.info(` doppler: auto-provisioned env for ${slug}`);
|
|
177
177
|
}
|
|
178
|
-
for (const cmd of readPostCreate(workbenchRoot)) {
|
|
178
|
+
for (const cmd of readPostCreate(workbenchRoot, repo)) {
|
|
179
179
|
console.info(` post_create: ${cmd}`);
|
|
180
180
|
const pc = spawnSync(cmd, { cwd: worktreeDir, stdio: "inherit", shell: true });
|
|
181
181
|
if (pc.status !== 0) {
|
|
@@ -192,9 +192,16 @@ function worktreeCreateResolved(slug, baseBranch, repo) {
|
|
|
192
192
|
}
|
|
193
193
|
process.exit(code);
|
|
194
194
|
}
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Read `post_create` commands from the worktree config of THE repo being
|
|
197
|
+
* created — run in each new worktree after create.
|
|
198
|
+
*
|
|
199
|
+
* It read `repos[0]`'s config for every repo, so in a multi-repo workbench
|
|
200
|
+
* `worktree create beta <slug>` ran alpha's install steps in beta's worktree
|
|
201
|
+
* (workbench-trust-fixes, F6 / A15). The caller names the repo; with none
|
|
202
|
+
* named the bash script has already refused, so there is nothing to run.
|
|
203
|
+
*/
|
|
204
|
+
export function readPostCreate(workbenchRoot, repo) {
|
|
198
205
|
if (!repo)
|
|
199
206
|
return [];
|
|
200
207
|
const p = join(workbenchRoot, ".indusk", "worktree-configs", `${repo}.json`);
|
|
@@ -210,7 +217,7 @@ function readPostCreate(workbenchRoot) {
|
|
|
210
217
|
return [];
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
|
-
/** Walk up from `start` to the nearest workbench root
|
|
220
|
+
/** Walk up from `start` to the nearest workbench root — the one `isWorkbench` rule, so a shapeless `repos[]` config counts too. */
|
|
214
221
|
function resolveWorkbenchRoot(start) {
|
|
215
222
|
let dir = resolve(start);
|
|
216
223
|
for (let i = 0; i < 40; i++) {
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getCleanupConfig, resolveCapForPath } from "../config.js";
|
|
5
|
+
import { isWorkbench, readWorkbenchRepos, repoDir } from "../worktree/repos.js";
|
|
5
6
|
/** Run git; swallow errors to an empty string (callers tolerate absence).
|
|
6
7
|
* stderr is ignored — execFileSync forwards child stderr to the parent's
|
|
7
8
|
* terminal by default, which leaked raw `git diff` usage spam (round-2 F1). */
|
|
@@ -86,15 +87,26 @@ function isGeneratedOrVendored(rel) {
|
|
|
86
87
|
* history) it falls back to `baseRef` directly.
|
|
87
88
|
*/
|
|
88
89
|
export function listOversizedChangedFiles(projectRoot, baseRef = "origin/main") {
|
|
89
|
-
// Fail loudly on a non-git root
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// the same statePath/gitPath split the eval-rail hit in 1.31.7/1.31.12).
|
|
90
|
+
// Fail loudly on a non-git root — silently returning [] would make the
|
|
91
|
+
// ritual report "nothing to clean" for a tree it never examined (round-2
|
|
92
|
+
// F1, the same statePath/gitPath split the eval rail hit in 1.31.7/1.31.12).
|
|
93
93
|
if (!git(projectRoot, ["rev-parse", "--show-toplevel"])) {
|
|
94
94
|
throw new Error(`listOversizedChangedFiles: ${projectRoot} is not a git repo. ` +
|
|
95
95
|
"In workbench mode pass the wrapped repo/worktree path (where the code lives), " +
|
|
96
96
|
"not the workbench root (where .indusk/ lives).");
|
|
97
97
|
}
|
|
98
|
+
// A versioned workbench root IS a git repo, so the guard above passes and
|
|
99
|
+
// the diff it would examine is the plan documents — the code lives in repos
|
|
100
|
+
// the workbench's git ignores. Returning [] there is "checked and clean" for
|
|
101
|
+
// code that was never seen. `verify` had the identical gap and refuses by
|
|
102
|
+
// declaration (`resolveVerifyRoots`); this is cleanup's maintained refusal
|
|
103
|
+
// (workbench-trust-fixes, F3).
|
|
104
|
+
if (isWorkbench(projectRoot)) {
|
|
105
|
+
const dirs = readWorkbenchRepos(projectRoot).map(repoDir);
|
|
106
|
+
throw new Error(`listOversizedChangedFiles: ${projectRoot} is a workbench — its code lives in ${dirs.join(", ") || "repos it does not declare"}, ` +
|
|
107
|
+
"not in this repository, so a diff here cannot contain it. Refusing rather than reporting nothing to clean. " +
|
|
108
|
+
"Run the cleanup scan inside the repository the plan's code lives in.");
|
|
109
|
+
}
|
|
98
110
|
const mergeBase = resolveMergeBase(projectRoot, baseRef);
|
|
99
111
|
const names = new Set();
|
|
100
112
|
const ranges = [[`${mergeBase}..HEAD`], ["--cached"], []];
|
|
@@ -10,8 +10,9 @@ export interface EvaluatorRunOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Where `.indusk/` lives — the InDusk state root. All state paths
|
|
12
12
|
* (results.log, evaluator-session.json, config.json, highlights) hang off
|
|
13
|
-
* this. In workbench mode this is the workbench root (
|
|
14
|
-
* in single-repo mode it
|
|
13
|
+
* this. In workbench mode this is the workbench root (whose own git
|
|
14
|
+
* history is plan documents, never the code); in single-repo mode it
|
|
15
|
+
* equals `gitRoot`.
|
|
15
16
|
*
|
|
16
17
|
* CONTRACT: `hooks/eval-trigger.js` MUST pass this key as `projectRoot`.
|
|
17
18
|
* It passed `statePath` between 1.31.7 and 1.31.11, leaving `projectRoot`
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync, readdirSync } from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import { readWorkbenchRepos } from "./worktree/repos.js";
|
|
24
|
+
import { readWorkbenchRepos, repoDir } from "./worktree/repos.js";
|
|
25
25
|
/**
|
|
26
26
|
* Find stray `.indusk/` directories in a workbench-shaped project.
|
|
27
27
|
*
|
|
@@ -45,7 +45,10 @@ export function findStrayState(workbenchRoot) {
|
|
|
45
45
|
return [];
|
|
46
46
|
const findings = [];
|
|
47
47
|
for (const repo of repos) {
|
|
48
|
-
|
|
48
|
+
// At the declared location (`path`, else name): by name, a repo declared
|
|
49
|
+
// elsewhere was a directory that did not exist, silently skipped, and
|
|
50
|
+
// the audit reported clean (workbench-trust-fixes, F6 / A12).
|
|
51
|
+
const wrappedRepoPath = join(workbenchRoot, repoDir(repo));
|
|
49
52
|
if (!existsSync(wrappedRepoPath))
|
|
50
53
|
continue;
|
|
51
54
|
const strayInsideWrapped = walkForStrayIndusk(wrappedRepoPath, 2);
|
|
@@ -35,12 +35,6 @@ export declare function computePhaseCloseBlockers(body: string, phase: number):
|
|
|
35
35
|
row: TrajectoryRow;
|
|
36
36
|
message: string;
|
|
37
37
|
}[];
|
|
38
|
-
/**
|
|
39
|
-
* Convenience: nudge text for phase start. Returns a string listing rows
|
|
40
|
-
* whose `Writable at` equals the phase and are not yet written, or null
|
|
41
|
-
* if nothing needs authoring.
|
|
42
|
-
*/
|
|
43
|
-
export declare function getPhaseStartNudge(body: string, phase: number): string | null;
|
|
44
38
|
/**
|
|
45
39
|
* Convenience: nudge text for near-phase-close. Returns a string listing
|
|
46
40
|
* rows whose `Passes at` equals the phase but state is not yet `passing`
|
|
@@ -100,21 +100,6 @@ export function computePhaseCloseBlockers(body, phase) {
|
|
|
100
100
|
message: ` [${row.id}] ${row.asserts} — state: ${row.state} (must be 'passing' or 'skipped' to close Phase ${phase})`,
|
|
101
101
|
}));
|
|
102
102
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Convenience: nudge text for phase start. Returns a string listing rows
|
|
105
|
-
* whose `Writable at` equals the phase and are not yet written, or null
|
|
106
|
-
* if nothing needs authoring.
|
|
107
|
-
*/
|
|
108
|
-
export function getPhaseStartNudge(body, phase) {
|
|
109
|
-
const trajectory = parseTrajectory(body);
|
|
110
|
-
if (!trajectory.present)
|
|
111
|
-
return null;
|
|
112
|
-
const rows = getRowsWritableAt(trajectory, phase);
|
|
113
|
-
if (rows.length === 0)
|
|
114
|
-
return null;
|
|
115
|
-
const lines = rows.map((r) => ` [${r.id}] ${r.asserts}`);
|
|
116
|
-
return `Phase ${phase} opens with these tests to author (commit as failing before implementation work):\n${lines.join("\n")}`;
|
|
117
|
-
}
|
|
118
103
|
/**
|
|
119
104
|
* Convenience: nudge text for near-phase-close. Returns a string listing
|
|
120
105
|
* rows whose `Passes at` equals the phase but state is not yet `passing`
|
package/dist/lib/verify/roots.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { isWorkbench, readWorkbenchRepos } from "../worktree/repos.js";
|
|
2
|
+
import { isWorkbench, readWorkbenchRepos, repoDir, resolveReposRoot } from "../worktree/repos.js";
|
|
3
3
|
/**
|
|
4
4
|
* Resolve both roots, or refuse.
|
|
5
5
|
*
|
|
@@ -46,7 +46,10 @@ export function resolveVerifyRoots(planRoot) {
|
|
|
46
46
|
error: `${planRoot} is a workbench: its plan documents and its code (${declared}) live in different repositories, ` +
|
|
47
47
|
"and the verify ledger records a baseline from the plan repo that has no meaning in the code repo. " +
|
|
48
48
|
"Refusing rather than judging code against a diff that cannot contain it. " +
|
|
49
|
-
|
|
49
|
+
// The checkout's real location — `repos_root` + declared `path` — not
|
|
50
|
+
// `<planRoot>/<name>`, which exists only on the flat legacy layout
|
|
51
|
+
// (workbench-trust-fixes, A13).
|
|
52
|
+
`Run verify inside ${join(resolveReposRoot(planRoot), repoDir(repos[0]))} instead; cross-repo verification is a named follow-on.`,
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
55
|
export function isRefusal(r) {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* one, and a trunk link that was not made reports as one that was.
|
|
17
17
|
*/
|
|
18
18
|
import { spawnSync } from "node:child_process";
|
|
19
|
-
import { existsSync, lstatSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, } from "node:fs";
|
|
20
|
-
import { join, relative } from "node:path";
|
|
19
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, } from "node:fs";
|
|
20
|
+
import { dirname, join, relative } from "node:path";
|
|
21
21
|
/**
|
|
22
22
|
* Root entries that are never worktrees.
|
|
23
23
|
*
|
|
@@ -120,7 +120,12 @@ export function isDanglingLink(p) {
|
|
|
120
120
|
*/
|
|
121
121
|
export function linkTrunk(workbenchRoot, name, target) {
|
|
122
122
|
const link = join(workbenchRoot, name);
|
|
123
|
-
|
|
123
|
+
// A symlink's relative target resolves from the link's OWN directory, not
|
|
124
|
+
// the workbench root — the two differ the moment `name` has more than one
|
|
125
|
+
// segment (`code/alpha`), and a link computed from the root pointed at
|
|
126
|
+
// itself. The parent must also exist before the link can (falsification
|
|
127
|
+
// A22; nothing in a flat layout could reach either).
|
|
128
|
+
const rel = relative(dirname(link), target);
|
|
124
129
|
if (existsSync(link) || isDanglingLink(link)) {
|
|
125
130
|
// A correct link needs nothing doing — report it as linked.
|
|
126
131
|
if (isSymlink(link) && readlinkSync(link) === rel)
|
|
@@ -131,6 +136,7 @@ export function linkTrunk(workbenchRoot, name, target) {
|
|
|
131
136
|
else
|
|
132
137
|
return false;
|
|
133
138
|
}
|
|
139
|
+
mkdirSync(dirname(link), { recursive: true });
|
|
134
140
|
symlinkSync(rel, link);
|
|
135
141
|
return true;
|
|
136
142
|
}
|
|
@@ -17,7 +17,16 @@ export type { WorkbenchRepo } from "../config.js";
|
|
|
17
17
|
* "not a workbench" exactly as they read a missing `wrapped_repo` today.
|
|
18
18
|
*/
|
|
19
19
|
export declare function readWorkbenchRepos(root: string): WorkbenchRepo[];
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Whether this project is workbench-shaped at all.
|
|
22
|
+
*
|
|
23
|
+
* `shape: "workbench"` says so explicitly; declaring repos says so just as
|
|
24
|
+
* loudly. A config with `repos[]` and no `shape` flag used to read as a flat
|
|
25
|
+
* project here while every other reader (`readWorkbenchRepos`, the bash lane,
|
|
26
|
+
* the eval hook) treated it as a workbench — so `verify` would have judged the
|
|
27
|
+
* wrapper repo's plan documents as code (workbench-trust-fixes, F6 / A14). One
|
|
28
|
+
* rule, and every refusal inherits it through this function.
|
|
29
|
+
*/
|
|
21
30
|
export declare function isWorkbench(root: string): boolean;
|
|
22
31
|
/** The parent directory the sibling clones live in, or null when undeclared. */
|
|
23
32
|
export declare function readSiblingParent(root: string): string | null;
|
|
@@ -64,9 +64,20 @@ export function readWorkbenchRepos(root) {
|
|
|
64
64
|
}
|
|
65
65
|
return repos;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Whether this project is workbench-shaped at all.
|
|
69
|
+
*
|
|
70
|
+
* `shape: "workbench"` says so explicitly; declaring repos says so just as
|
|
71
|
+
* loudly. A config with `repos[]` and no `shape` flag used to read as a flat
|
|
72
|
+
* project here while every other reader (`readWorkbenchRepos`, the bash lane,
|
|
73
|
+
* the eval hook) treated it as a workbench — so `verify` would have judged the
|
|
74
|
+
* wrapper repo's plan documents as code (workbench-trust-fixes, F6 / A14). One
|
|
75
|
+
* rule, and every refusal inherits it through this function.
|
|
76
|
+
*/
|
|
68
77
|
export function isWorkbench(root) {
|
|
69
|
-
|
|
78
|
+
if (readRawConfig(root)?.worktree?.shape === "workbench")
|
|
79
|
+
return true;
|
|
80
|
+
return readWorkbenchRepos(root).length > 0;
|
|
70
81
|
}
|
|
71
82
|
/** The parent directory the sibling clones live in, or null when undeclared. */
|
|
72
83
|
export function readSiblingParent(root) {
|
|
@@ -12,7 +12,9 @@ export declare function allLocationsDeclared(repos: readonly WorkbenchRepo[]): b
|
|
|
12
12
|
*
|
|
13
13
|
* Returns true when it topped up.
|
|
14
14
|
*/
|
|
15
|
-
export declare function topUpManagedIgnore(workbenchRoot: string
|
|
15
|
+
export declare function topUpManagedIgnore(workbenchRoot: string, opts?: {
|
|
16
|
+
layoutDeclared?: boolean;
|
|
17
|
+
}): boolean;
|
|
16
18
|
export interface ScaffoldResult {
|
|
17
19
|
created: string[];
|
|
18
20
|
kept: string[];
|
|
@@ -24,6 +24,14 @@ import { repoDir } from "./repos.js";
|
|
|
24
24
|
*/
|
|
25
25
|
const ROOT_DENY_RULE = "/*/";
|
|
26
26
|
const SECRETS_RULE = ".indusk/extensions/*/.env";
|
|
27
|
+
/**
|
|
28
|
+
* The worktree extension's editor schema, shipped beside the configs by its
|
|
29
|
+
* `on_enable` hook so each config's `$schema` resolves. Its contents track the
|
|
30
|
+
* installed package, so it is machine-local in the same sense as `.indusk/eval/`:
|
|
31
|
+
* real content, true only for this machine. Shared, two teammates on different
|
|
32
|
+
* versions rewrite it at each other on every enable.
|
|
33
|
+
*/
|
|
34
|
+
const WORKTREE_SCHEMA_RULE = ".indusk/worktree-configs/config.schema.json";
|
|
27
35
|
/**
|
|
28
36
|
* Root-level DIRECTORY whitelist, not a blacklist.
|
|
29
37
|
*
|
|
@@ -64,6 +72,7 @@ env/*.env
|
|
|
64
72
|
|
|
65
73
|
# Machine-local state — real content, but true only for this machine.
|
|
66
74
|
.indusk/eval/
|
|
75
|
+
${WORKTREE_SCHEMA_RULE}
|
|
67
76
|
.indusk/current.md.lock
|
|
68
77
|
.indusk/sync-stamp
|
|
69
78
|
.claude/settings.local.json
|
|
@@ -97,6 +106,7 @@ env/*.env
|
|
|
97
106
|
|
|
98
107
|
# Machine-local state — real content, but true only for this machine.
|
|
99
108
|
.indusk/eval/
|
|
109
|
+
${WORKTREE_SCHEMA_RULE}
|
|
100
110
|
.indusk/current.md.lock
|
|
101
111
|
.indusk/sync-stamp
|
|
102
112
|
.claude/settings.local.json
|
|
@@ -157,7 +167,29 @@ const GITATTRIBUTES = `# InDusk workbench context repo.
|
|
|
157
167
|
* appending deny-by-default to it would invert what they meant.
|
|
158
168
|
*/
|
|
159
169
|
const INDUSK_MANAGED_MARKER = "# InDusk managed";
|
|
160
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Rules EVERY workbench needs, whatever its layout.
|
|
172
|
+
*
|
|
173
|
+
* Split out of the flat block because they were unreachable from a declared
|
|
174
|
+
* layout: `refuseIfIgnoreCannotHold` returns early there — a declared layout
|
|
175
|
+
* needs no deny-by-default rule — and the top-up sat behind that return, so a
|
|
176
|
+
* declared workbench scaffolded before a rule existed could never receive it
|
|
177
|
+
* and went on offering the file to its shared remote. A guard that returns
|
|
178
|
+
* early for one layout must not carry unrelated work behind it.
|
|
179
|
+
*/
|
|
180
|
+
const MACHINE_LOCAL_RULES = `
|
|
181
|
+
# --- InDusk machine-local (generated) ---
|
|
182
|
+
# Real content, true only for this machine: package-owned files that track the
|
|
183
|
+
# installed version, and a lock.
|
|
184
|
+
${WORKTREE_SCHEMA_RULE}
|
|
185
|
+
.indusk/current.md.lock
|
|
186
|
+
`;
|
|
187
|
+
/**
|
|
188
|
+
* Rules only a FLAT workbench needs, in the order they should be appended.
|
|
189
|
+
*
|
|
190
|
+
* Deny-by-default belongs here and nowhere else. Appending it to a declared
|
|
191
|
+
* layout's ignore file would invert a file this module refuses to rewrite.
|
|
192
|
+
*/
|
|
161
193
|
const FLAT_WORKBENCH_RULES = `
|
|
162
194
|
# --- InDusk workbench (generated) ---
|
|
163
195
|
# Worktree directories are created at runtime, so they cannot be named in
|
|
@@ -172,7 +204,6 @@ ${ROOT_DENY_RULE}
|
|
|
172
204
|
${SECRETS_RULE}*
|
|
173
205
|
!${SECRETS_RULE}.example
|
|
174
206
|
env/*.env
|
|
175
|
-
.indusk/current.md.lock
|
|
176
207
|
`;
|
|
177
208
|
/**
|
|
178
209
|
* Add the flat-workbench rules to an ignore file InDusk already owns.
|
|
@@ -185,16 +216,29 @@ env/*.env
|
|
|
185
216
|
*
|
|
186
217
|
* Returns true when it topped up.
|
|
187
218
|
*/
|
|
188
|
-
export function topUpManagedIgnore(workbenchRoot) {
|
|
219
|
+
export function topUpManagedIgnore(workbenchRoot, opts = {}) {
|
|
189
220
|
const path = join(workbenchRoot, ".gitignore");
|
|
190
221
|
if (!existsSync(path))
|
|
191
222
|
return false;
|
|
192
223
|
const body = readFileSync(path, "utf-8");
|
|
193
224
|
if (!body.includes(INDUSK_MANAGED_MARKER))
|
|
194
225
|
return false; // a human's file — refuse elsewhere
|
|
195
|
-
|
|
226
|
+
// Per-RULE, not per-file. An earlier version returned early whenever the
|
|
227
|
+
// root rule was present, so every rule added after that one could never
|
|
228
|
+
// reach a workbench scaffolded before it — the file looked correct and was
|
|
229
|
+
// a release behind. Each rule the flat block carries is topped up on its
|
|
230
|
+
// own, in the block's own order.
|
|
231
|
+
// A declared layout gets the machine-local rules only; the flat block's
|
|
232
|
+
// deny-by-default rule is the one thing that must never reach it.
|
|
233
|
+
const blocks = opts.layoutDeclared
|
|
234
|
+
? [MACHINE_LOCAL_RULES]
|
|
235
|
+
: [FLAT_WORKBENCH_RULES, MACHINE_LOCAL_RULES];
|
|
236
|
+
const missing = blocks
|
|
237
|
+
.flatMap((block) => block.split("\n"))
|
|
238
|
+
.filter((line) => line.trim() !== "" && !line.startsWith("#") && !body.includes(line));
|
|
239
|
+
if (missing.length === 0)
|
|
196
240
|
return false; // already correct
|
|
197
|
-
appendFileSync(path,
|
|
241
|
+
appendFileSync(path, `${missing.join("\n")}\n`);
|
|
198
242
|
return true;
|
|
199
243
|
}
|
|
200
244
|
/**
|
|
@@ -268,6 +312,10 @@ export function missingIgnoreRules(workbenchRoot) {
|
|
|
268
312
|
"the root is not deny-by-default, so the next worktree directory gets committed",
|
|
269
313
|
],
|
|
270
314
|
[SECRETS_RULE, "extension secrets are not ignored"],
|
|
315
|
+
[
|
|
316
|
+
WORKTREE_SCHEMA_RULE,
|
|
317
|
+
"the worktree config schema is package-owned and would be committed, so teammates on different versions rewrite it at each other",
|
|
318
|
+
],
|
|
271
319
|
];
|
|
272
320
|
return required.filter(([rule]) => !body.includes(rule)).map(([rule, why]) => `${rule} — ${why}`);
|
|
273
321
|
}
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
# `indusk update` re-copies if upstream changes)
|
|
10
10
|
# 3. Registers `wt`, `wt:pm2`, `preflight` scripts in the workbench's
|
|
11
11
|
# package.json (merges, doesn't duplicate)
|
|
12
|
-
# 4.
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# 4. Ships `.indusk/worktree-configs/config.schema.json` (the editor
|
|
13
|
+
# schema the starter config's `$schema` points at), refreshed on every
|
|
14
|
+
# enable; then materializes a starter
|
|
15
|
+
# `.indusk/worktree-configs/<wrapped_repo>.json` from the template if
|
|
16
|
+
# absent — substituting WRAPPED_REPO_NAME with the actual repo name
|
|
17
|
+
# from worktree.wrapped_repo config
|
|
15
18
|
#
|
|
16
19
|
# Requires:
|
|
17
20
|
# - jq installed
|
|
@@ -76,8 +79,18 @@ else
|
|
|
76
79
|
echo " WARN: no package.json at $PKG_JSON; skipping script registration"
|
|
77
80
|
fi
|
|
78
81
|
|
|
79
|
-
# 3.
|
|
82
|
+
# 3. Ship the config schema beside the configs, refreshed on every enable.
|
|
83
|
+
# The starter config's `$schema` points at `./config.schema.json`; enabling
|
|
84
|
+
# an extension copies only its manifest into the project, so nothing else
|
|
85
|
+
# puts the schema anywhere a relative pointer could reach. A config that
|
|
86
|
+
# already exists is left alone below; the schema is always refreshed so a
|
|
87
|
+
# package upgrade carries schema changes into the project.
|
|
80
88
|
CONFIG_DIR="$WORKBENCH_ROOT/.indusk/worktree-configs"
|
|
89
|
+
mkdir -p "$CONFIG_DIR"
|
|
90
|
+
cp "$EXT_DIR/config.schema.json" "$CONFIG_DIR/config.schema.json"
|
|
91
|
+
echo " schema: $CONFIG_DIR/config.schema.json"
|
|
92
|
+
|
|
93
|
+
# 4. Materialize starter worktree-config if absent.
|
|
81
94
|
for REPO_NAME in ${DECLARED_REPOS+"${DECLARED_REPOS[@]}"}; do
|
|
82
95
|
CONFIG_FILE="$CONFIG_DIR/${REPO_NAME}.json"
|
|
83
96
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"on_enable": "indusk worktree _on-enable",
|
|
10
|
+
"on_update": "indusk worktree _on-enable",
|
|
10
11
|
"on_disable": "echo 'worktree extension disabled — scripts in <workbench>/scripts/worktree/ + package.json entries are left in place for manual cleanup'"
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -268,6 +268,57 @@ _read_workbench_repo_paths() {
|
|
|
268
268
|
' "$config" 2>/dev/null || echo ""
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
# The worktrees directory <repo> DECLARES, relative to the workbench root, or
|
|
272
|
+
# empty when it declares none (worktrees then live at the root). One reader so
|
|
273
|
+
# setup-worktree.sh run directly agrees with the TS wrapper that passes
|
|
274
|
+
# --worktrees-dir (workbench-trust-fixes, F5).
|
|
275
|
+
_wt_declared_worktrees_dir() {
|
|
276
|
+
local repo="$1" name dir
|
|
277
|
+
while IFS=$'\t' read -r name dir; do
|
|
278
|
+
[[ "$name" == "$repo" ]] || continue
|
|
279
|
+
echo "$dir"
|
|
280
|
+
return 0
|
|
281
|
+
done < <(_read_workbench_worktree_dirs)
|
|
282
|
+
echo ""
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Every directory that could be a worktree: the workbench root's entries minus
|
|
286
|
+
# the reserved names, plus every entry of every DECLARED worktrees dir. Callers
|
|
287
|
+
# that need "all worktrees" (refresh --all) iterate this rather than scanning
|
|
288
|
+
# the root alone, which could not see a declared worktrees dir at all.
|
|
289
|
+
_wt_list_worktree_dirs() {
|
|
290
|
+
local entry name dir _name path
|
|
291
|
+
# Root entries that are declared LOCATIONS rather than worktrees: a declared
|
|
292
|
+
# worktrees dir itself, and the first segment of a declared repo path
|
|
293
|
+
# (`code` for `code/alpha`). Listing them as candidates produced a SKIP line
|
|
294
|
+
# per layout element on every `refresh --all` (falsification A23).
|
|
295
|
+
local -a declared_dirs=()
|
|
296
|
+
while IFS=$'\t' read -r _name dir; do
|
|
297
|
+
[[ -n "$dir" ]] && declared_dirs+=("${dir%%/*}")
|
|
298
|
+
done < <(_read_workbench_worktree_dirs)
|
|
299
|
+
while IFS=$'\t' read -r _name path; do
|
|
300
|
+
[[ -n "$path" ]] && declared_dirs+=("${path%%/*}")
|
|
301
|
+
done < <(_read_workbench_repo_paths)
|
|
302
|
+
for entry in "$WORKBENCH_ROOT"/*; do
|
|
303
|
+
[[ -d "$entry" ]] || continue
|
|
304
|
+
name="$(basename "$entry")"
|
|
305
|
+
_wt_is_reserved_name "$name" && continue
|
|
306
|
+
local skip=0 d
|
|
307
|
+
for d in "${declared_dirs[@]+"${declared_dirs[@]}"}"; do
|
|
308
|
+
[[ "$name" == "$d" ]] && skip=1
|
|
309
|
+
done
|
|
310
|
+
[[ "$skip" -eq 1 ]] && continue
|
|
311
|
+
echo "$entry"
|
|
312
|
+
done
|
|
313
|
+
while IFS=$'\t' read -r _name dir; do
|
|
314
|
+
[[ -n "$dir" && -d "$WORKBENCH_ROOT/$dir" ]] || continue
|
|
315
|
+
for entry in "$WORKBENCH_ROOT/$dir"/*; do
|
|
316
|
+
[[ -d "$entry" ]] || continue
|
|
317
|
+
echo "$entry"
|
|
318
|
+
done
|
|
319
|
+
done < <(_read_workbench_worktree_dirs)
|
|
320
|
+
}
|
|
321
|
+
|
|
271
322
|
# Root entries that are never worktrees. ONE definition for the bash lane —
|
|
272
323
|
# wt.sh and wt-pm2.sh each carried a copy, and they had already drifted from
|
|
273
324
|
# RESERVED_ROOT_DIRS in src/lib/worktree/layout.ts (`docs` was missing from
|
|
@@ -283,7 +334,7 @@ _wt_is_reserved_name() {
|
|
|
283
334
|
# 1. The declared workbench-side location: <workbench>/<path-or-name>,
|
|
284
335
|
# which is the trunk symlink in a linked layout or the real directory in
|
|
285
336
|
# a nested one (`repos_root: "."`).
|
|
286
|
-
# 2. Else <repos_root>/<name> — the checkout itself, for a workbench whose
|
|
337
|
+
# 2. Else <repos_root>/<path-or-name> — the checkout itself, for a workbench whose
|
|
287
338
|
# trunk link was never made. Routing there is what "the config says where
|
|
288
339
|
# things are" means; failing because a symlink is missing is scan-luck.
|
|
289
340
|
_wt_resolve_trunk_dir() {
|
|
@@ -301,11 +352,14 @@ _wt_resolve_trunk_dir() {
|
|
|
301
352
|
fi
|
|
302
353
|
local repos_root
|
|
303
354
|
repos_root="$(_read_repos_root)" || return 1
|
|
304
|
-
|
|
305
|
-
|
|
355
|
+
# The checkout itself, at its DECLARED location under repos_root — not by
|
|
356
|
+
# name, which missed every repo declared at a `path` on a sibling layout
|
|
357
|
+
# (workbench-trust-fixes, F5).
|
|
358
|
+
if [[ -d "$repos_root/${declared_path:-$repo}" ]]; then
|
|
359
|
+
echo "$repos_root/${declared_path:-$repo}"
|
|
306
360
|
return 0
|
|
307
361
|
fi
|
|
308
|
-
echo "Error: trunk for repo '$repo' not found — looked at $side and $repos_root/$repo. Run \`indusk workbench restore\`." >&2
|
|
362
|
+
echo "Error: trunk for repo '$repo' not found — looked at $side and $repos_root/${declared_path:-$repo}. Run \`indusk workbench restore\`." >&2
|
|
309
363
|
return 1
|
|
310
364
|
}
|
|
311
365
|
|