@infinitedusky/indusk-mcp 1.44.0 → 1.44.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.
Files changed (49) hide show
  1. package/admin/.next/BUILD_ID +1 -1
  2. package/admin/.next/build-manifest.json +3 -3
  3. package/admin/.next/fallback-build-manifest.json +3 -3
  4. package/admin/.next/prerender-manifest.json +3 -3
  5. package/admin/.next/server/app/_global-error.html +1 -1
  6. package/admin/.next/server/app/_global-error.rsc +1 -1
  7. package/admin/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/admin/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/admin/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/admin/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/admin/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/admin/.next/server/middleware-build-manifest.js +3 -3
  13. package/admin/.next/server/pages/500.html +1 -1
  14. package/admin/.next/server/server-reference-manifest.js +1 -1
  15. package/admin/.next/server/server-reference-manifest.json +1 -1
  16. package/admin/.next/trace +1 -1
  17. package/admin/.next/trace-build +1 -1
  18. package/dist/bin/commands/agent.js +3 -3
  19. package/dist/bin/commands/run.js +21 -0
  20. package/dist/bin/commands/workbench.d.ts +0 -8
  21. package/dist/bin/commands/workbench.js +19 -5
  22. package/dist/bin/commands/worktree.d.ts +10 -0
  23. package/dist/bin/commands/worktree.js +12 -5
  24. package/dist/lib/cleanup/oversized.js +16 -4
  25. package/dist/lib/eval/evaluator-runner.d.ts +3 -2
  26. package/dist/lib/stray-state-audit.js +5 -2
  27. package/dist/lib/trajectory/state-ops.d.ts +0 -6
  28. package/dist/lib/trajectory/state-ops.js +0 -15
  29. package/dist/lib/verify/roots.js +5 -2
  30. package/dist/lib/worktree/layout.js +9 -3
  31. package/dist/lib/worktree/repos.d.ts +10 -1
  32. package/dist/lib/worktree/repos.js +13 -2
  33. package/extensions/worktree/hooks/on_enable.sh +17 -4
  34. package/extensions/worktree/scripts/lib/workbench-helpers.sh +58 -4
  35. package/extensions/worktree/scripts/preflight.sh +10 -46
  36. package/extensions/worktree/scripts/refresh-worktree.sh +18 -10
  37. package/extensions/worktree/scripts/setup-worktree.sh +11 -3
  38. package/extensions/worktree/templates/worktree-config.template.json +1 -1
  39. package/hooks/_hook-paths.js +129 -17
  40. package/hooks/_impl-phases.js +88 -0
  41. package/hooks/check-gates.js +3 -70
  42. package/hooks/eval-trigger.js +31 -5
  43. package/hooks/gate-reminder.js +92 -187
  44. package/package.json +1 -1
  45. package/skills/cleanup.md +1 -1
  46. package/skills/retrospective.md +1 -0
  47. /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → GeLihB_OOFljBvLKtTR-j}/_buildManifest.js +0 -0
  48. /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → GeLihB_OOFljBvLKtTR-j}/_clientMiddlewareManifest.js +0 -0
  49. /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → GeLihB_OOFljBvLKtTR-j}/_ssgManifest.js +0 -0
@@ -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
- /** Read `post_create` commands from the workbench's worktree config — run in each new worktree after create. */
196
- function readPostCreate(workbenchRoot) {
197
- const repo = readWorkbenchRepos(workbenchRoot)[0]?.name;
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 (`.indusk/config.json` with worktree.shape === "workbench"). */
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. A workbench root (where .indusk/ lives) is
90
- // deliberately NOT a git repo — silently returning [] there would make the
91
- // ritual report "nothing to clean" on every workbench project (round-2 F1,
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 (NOT a git repo);
14
- * in single-repo mode it equals `gitRoot`.
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
- const wrappedRepoPath = join(workbenchRoot, repo.name);
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`
@@ -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
- `Run verify inside ${join(planRoot, declared)} instead; cross-repo verification is a named follow-on.`,
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
- const rel = relative(workbenchRoot, target);
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
- /** Whether this project is workbench-shaped at all. */
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
- /** Whether this project is workbench-shaped at all. */
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
- return readRawConfig(root)?.worktree?.shape === "workbench";
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) {
@@ -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. Materializes a starter `.indusk/worktree-configs/<wrapped_repo>.json`
13
- # from the template if absent — substituting WRAPPED_REPO_NAME with
14
- # the actual repo name from worktree.wrapped_repo config
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. Materialize starter worktree-config if absent.
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
@@ -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
- if [[ -d "$repos_root/$repo" ]]; then
305
- echo "$repos_root/$repo"
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
 
@@ -61,55 +61,19 @@ if [[ ! -f "$CONFIG_FILE" ]]; then
61
61
  exit 1
62
62
  fi
63
63
 
64
- # Reject preflighting the trunk — preflight is a feature-branch concept.
65
- if [[ "$SLUG" == "$REPO" ]]; then
64
+ # One resolution surface — the root and every declared worktrees dir, exact
65
+ # match first, `-<slug>` suffix as fallback, ambiguity refused by name — the
66
+ # same resolver behind `wt` (workbench-trust-fixes, F5). The private root-only
67
+ # scan that stood here could not see a declared worktrees dir and carried its
68
+ # own reserved-name list, already drifted from the shared one.
69
+ WORKTREE_PATH="$(_wt_resolve_target "$SLUG")" || exit 1
70
+ TRUNK_DIR="$(_wt_resolve_trunk_dir "$REPO")" || exit 1
71
+ # Reject preflighting the trunk — preflight is a feature-branch concept. By
72
+ # resolved path: under a declared `path` the trunk's basename is not the name.
73
+ if [[ "$(cd "$WORKTREE_PATH" && pwd -P)" == "$(cd "$TRUNK_DIR" && pwd -P)" ]]; then
66
74
  echo "Error: preflight cannot target the trunk; pick a worktree slug" >&2
67
75
  exit 1
68
76
  fi
69
-
70
- # Slug resolution (single-pass against workbench root; same as wt.sh).
71
- _is_reserved_name() {
72
- case "$1" in
73
- .indusk | .claude | .vscode | .cursor | node_modules | dist | build | .git | .next | scripts | env) return 0 ;;
74
- *) return 1 ;;
75
- esac
76
- }
77
-
78
- exact_paths=()
79
- suffix_paths=()
80
- for entry in "$WORKBENCH_ROOT"/*; do
81
- [[ -d "$entry" ]] || continue
82
- name="$(basename "$entry")"
83
- _is_reserved_name "$name" && continue
84
- # Skip the trunk symlink — preflight is for feature worktrees.
85
- [[ "$name" == "$REPO" ]] && continue
86
- if [[ "$name" == "$SLUG" ]]; then
87
- exact_paths+=("$entry")
88
- elif [[ "$name" == *"-$SLUG" ]]; then
89
- suffix_paths+=("$entry")
90
- fi
91
- done
92
-
93
- candidate_paths=()
94
- if [[ ${#exact_paths[@]} -gt 0 ]]; then
95
- candidate_paths=("${exact_paths[@]}")
96
- elif [[ ${#suffix_paths[@]} -gt 0 ]]; then
97
- candidate_paths=("${suffix_paths[@]}")
98
- fi
99
-
100
- if [[ ${#candidate_paths[@]} -eq 0 ]]; then
101
- echo "Error: no worktree matching slug '$SLUG' at $WORKBENCH_ROOT" >&2
102
- exit 1
103
- fi
104
- if [[ ${#candidate_paths[@]} -gt 1 ]]; then
105
- echo "Error: multiple worktrees match slug '$SLUG':" >&2
106
- for p in "${candidate_paths[@]}"; do
107
- printf ' %s\n' "$(basename "$p")" >&2
108
- done
109
- exit 1
110
- fi
111
-
112
- WORKTREE_PATH="${candidate_paths[0]}"
113
77
  WORKTREE_NAME="$(basename "$WORKTREE_PATH")"
114
78
 
115
79
  # Read preflight[] from config. Each entry: { name, command, when? }.
@@ -36,7 +36,9 @@ export WORKBENCH_ROOT
36
36
  REPO="$(_resolve_workbench_repo "${REPO_ARG:-}")"
37
37
 
38
38
  SIBLING_PARENT="$(_read_repos_root)"
39
- CLIENT_ROOT="$SIBLING_PARENT/$REPO"
39
+ # Trunk from CONFIG (declared `path` honored) — workbench-trust-fixes, F5.
40
+ CLIENT_ROOT="$(_wt_resolve_trunk_dir "$REPO")"
41
+ CLIENT_ROOT_REAL="$(cd "$CLIENT_ROOT" && pwd -P)"
40
42
 
41
43
  CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
42
44
  if [[ ! -f "$CONFIG_FILE" ]]; then
@@ -54,8 +56,10 @@ refresh_one() {
54
56
  echo " SKIP: $slug — directory not found"
55
57
  return 0
56
58
  fi
57
- if [[ "$slug" == "$REPO" ]]; then
58
- return 0 # trunk symlink, not a worktree
59
+ # The trunk is excluded by resolved path, not by name — under a declared
60
+ # `path` its basename need not be the repo name.
61
+ if [[ "$(cd "$worktree_path" && pwd -P)" == "$CLIENT_ROOT_REAL" ]]; then
62
+ return 0 # the trunk checkout, not a worktree
59
63
  fi
60
64
  # Check this is actually a git worktree (has a .git file pointing into
61
65
  # the canonical clone's worktrees/ dir).
@@ -65,7 +69,7 @@ refresh_one() {
65
69
  fi
66
70
 
67
71
  echo ""
68
- echo "Refreshing: $slug"
72
+ echo "Refreshing: $slug ($worktree_path)"
69
73
 
70
74
  local gitdir
71
75
  gitdir="$(cd "$worktree_path" && git rev-parse --git-dir)"
@@ -180,13 +184,17 @@ refresh_one() {
180
184
  }
181
185
 
182
186
  if [[ "$ARG" == "--all" ]]; then
183
- echo "Refreshing all worktrees in $WORKBENCH_ROOT (excluding the trunk symlink)..."
184
- for d in "$WORKBENCH_ROOT"/*/; do
185
- [[ -d "$d" ]] || continue
186
- refresh_one "${d%/}"
187
- done
187
+ echo "Refreshing all worktrees in $WORKBENCH_ROOT and its declared worktrees dirs (excluding the trunk)..."
188
+ while IFS= read -r d; do
189
+ [[ -n "$d" ]] && refresh_one "$d"
190
+ done < <(_wt_list_worktree_dirs)
188
191
  else
189
- refresh_one "$WORKBENCH_ROOT/$ARG"
192
+ # One resolution surface for a slug — the root and every declared worktrees
193
+ # dir, exact match first, `-<slug>` suffix as fallback, ambiguity refused by
194
+ # name — the resolver behind `wt` (workbench-trust-fixes, F5). A slug that
195
+ # resolves to nothing is an error, not a SKIP that exits 0.
196
+ target="$(_wt_resolve_target "$ARG")" || exit 1
197
+ refresh_one "$target"
190
198
  fi
191
199
 
192
200
  echo ""
@@ -66,11 +66,19 @@ if [[ -z "$SIBLING_PARENT" ]]; then
66
66
  exit 1
67
67
  fi
68
68
 
69
- CLIENT_ROOT="$SIBLING_PARENT/$REPO"
70
- if [[ ! -d "$CLIENT_ROOT/.git" ]]; then
71
- echo "Error: $CLIENT_ROOT is not a git repo" >&2
69
+ # The trunk checkout from CONFIG — declared `path` honored — through the one
70
+ # resolver `wt` has used since 1.42.0. Building "$SIBLING_PARENT/$REPO" by
71
+ # name failed outright on any repo declared at a path (workbench-trust-fixes, F5).
72
+ CLIENT_ROOT="$(_wt_resolve_trunk_dir "$REPO")"
73
+ if [[ ! -e "$CLIENT_ROOT/.git" ]]; then
74
+ echo "Error: $CLIENT_ROOT is not a git checkout" >&2
72
75
  exit 1
73
76
  fi
77
+ # A declared worktrees location applies whether or not the TS wrapper passed
78
+ # --worktrees-dir — this script is also run directly.
79
+ if [[ -z "$WORKTREES_DIR" ]]; then
80
+ WORKTREES_DIR="$(_wt_declared_worktrees_dir "$REPO")"
81
+ fi
74
82
 
75
83
  CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
76
84
  if [[ ! -f "$CONFIG_FILE" ]]; then
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "../../config.schema.json",
2
+ "$schema": "./config.schema.json",
3
3
  "trunk_branch": "main",
4
4
  "base_branch": "main",
5
5
  "post_create": ["pnpm install"],