@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.
Files changed (55) 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/extensions.d.ts +11 -0
  20. package/dist/bin/commands/extensions.js +10 -0
  21. package/dist/bin/commands/run.js +21 -0
  22. package/dist/bin/commands/update.js +18 -14
  23. package/dist/bin/commands/workbench.d.ts +0 -8
  24. package/dist/bin/commands/workbench.js +28 -10
  25. package/dist/bin/commands/worktree.d.ts +10 -0
  26. package/dist/bin/commands/worktree.js +12 -5
  27. package/dist/lib/cleanup/oversized.js +16 -4
  28. package/dist/lib/eval/evaluator-runner.d.ts +3 -2
  29. package/dist/lib/stray-state-audit.js +5 -2
  30. package/dist/lib/trajectory/state-ops.d.ts +0 -6
  31. package/dist/lib/trajectory/state-ops.js +0 -15
  32. package/dist/lib/verify/roots.js +5 -2
  33. package/dist/lib/worktree/layout.js +9 -3
  34. package/dist/lib/worktree/repos.d.ts +10 -1
  35. package/dist/lib/worktree/repos.js +13 -2
  36. package/dist/lib/worktree/shareable.d.ts +3 -1
  37. package/dist/lib/worktree/shareable.js +53 -5
  38. package/extensions/worktree/hooks/on_enable.sh +17 -4
  39. package/extensions/worktree/manifest.json +1 -0
  40. package/extensions/worktree/scripts/lib/workbench-helpers.sh +58 -4
  41. package/extensions/worktree/scripts/preflight.sh +10 -46
  42. package/extensions/worktree/scripts/refresh-worktree.sh +18 -10
  43. package/extensions/worktree/scripts/setup-worktree.sh +11 -3
  44. package/extensions/worktree/templates/worktree-config.template.json +1 -1
  45. package/hooks/_hook-paths.js +129 -17
  46. package/hooks/_impl-phases.js +88 -0
  47. package/hooks/check-gates.js +3 -70
  48. package/hooks/eval-trigger.js +31 -5
  49. package/hooks/gate-reminder.js +92 -187
  50. package/package.json +2 -2
  51. package/skills/cleanup.md +1 -1
  52. package/skills/retrospective.md +1 -0
  53. /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_buildManifest.js +0 -0
  54. /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_clientMiddlewareManifest.js +0 -0
  55. /package/admin/.next/static/{4Mqj7sc33tNR898aibslD → F21UYqHRn4_drSf3X1wvn}/_ssgManifest.js +0 -0
@@ -1,222 +1,127 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PostToolUse hook: nudges the agent to call advance_plan when a phase is complete.
3
+ * PostToolUse hook on Edit|Write: the phase reminder.
4
4
  *
5
- * This hook is advisory — it cannot block. It outputs a reminder message
6
- * that appears in the conversation as additional context.
5
+ * When an edit to an `impl.md` closes a phase, tell the agent which trajectory
6
+ * rows the next phase opens with — the tests to author red before any code —
7
+ * and to call advance_plan. When a phase is mid-flight, name the rows that will
8
+ * block its close. Advisory: it never blocks.
9
+ *
10
+ * Delivery. A PostToolUse hook reaches the model in exactly one way at exit 0:
11
+ * a JSON envelope on stdout carrying `hookSpecificOutput.additionalContext`.
12
+ * stderr at exit 0 goes to the debug log and nowhere else. For its whole life
13
+ * this hook wrote to stderr and exited 0, so it was never once heard
14
+ * (workbench-trust-fixes, research F9). `console.info` writes to stdout and is
15
+ * on the linter's `noConsole` allowlist; a `console.log` here was swept once
16
+ * already and would be again.
17
+ *
18
+ * Phases. Two sequences ordered by document position (test-phase-structure):
19
+ * "the next phase" is the next heading in the document, whatever its kind, and
20
+ * trajectory cells are read through the shared `_trajectory-parser.js`, so
21
+ * `Build Phase 2`, `Test Phase 1` and `Phase 2` mean here exactly what they
22
+ * mean to check-gates. The nudge text has one definition — this file — pinned
23
+ * by `phase-start-nudge-single-definition.test.ts`.
7
24
  */
8
25
 
9
26
  import { readFileSync } from "node:fs";
10
- import { FORWARD_INTELLIGENCE_HEADING, gateHeading, PHASE_HEADING } from "./_impl-headings.js";
27
+ import { phaseOrdinal, phaseSequence } from "./_impl-headings.js";
28
+ import { parseImplPhases } from "./_impl-phases.js";
29
+ import { parseTrajectoryFromBody, stripFrontmatter } from "./_trajectory-parser.js";
11
30
 
12
- // Read hook input from stdin
13
31
  let input = "";
14
32
  for await (const chunk of process.stdin) {
15
33
  input += chunk;
16
34
  }
17
35
 
18
36
  const event = JSON.parse(input);
19
- const toolInput = event.tool_input ?? {};
20
- const filePath = toolInput.file_path ?? "";
37
+ const filePath = event.tool_input?.file_path ?? "";
21
38
 
22
- // Fast path: not an impl.md file
39
+ // Fast path: not an impl.md. Silence is the correct output here (A2).
23
40
  if (!filePath.endsWith("/impl.md") && !filePath.endsWith("\\impl.md")) {
24
41
  process.exit(0);
25
42
  }
26
43
 
27
- // Read the impl file (post-edit state)
28
44
  let content;
29
45
  try {
30
- content = readFileSync(filePath, "utf-8");
46
+ content = readFileSync(filePath, "utf-8"); // post-edit state
31
47
  } catch {
32
48
  process.exit(0);
33
49
  }
34
50
 
35
- // Parse phases
36
- function parsePhases(text) {
37
- const fmMatch = text.match(/^---\n[\s\S]*?\n---\n/);
38
- const body = fmMatch ? text.slice(fmMatch[0].length) : text;
39
-
40
- const lines = body.split("\n");
41
- const phases = [];
42
- let currentPhase = null;
43
- let currentGateType = "implementation";
44
-
45
- for (const line of lines) {
46
- const phaseMatch = line.match(PHASE_HEADING);
47
- if (phaseMatch) {
48
- if (currentPhase) phases.push(currentPhase);
49
- currentPhase = {
50
- number: parseInt(phaseMatch[1], 10),
51
- name: phaseMatch[2].trim(),
52
- items: [],
53
- };
54
- currentGateType = "implementation";
55
- continue;
56
- }
57
-
58
- // [1] is the phase number, [2] the gate kind — see gateHeading().
59
- const gateMatch = line.match(gateHeading("(Verification|Context|Document)"));
60
- if (gateMatch) {
61
- currentGateType = gateMatch[2].toLowerCase();
62
- continue;
63
- }
64
-
65
- if (line.match(FORWARD_INTELLIGENCE_HEADING)) {
66
- currentGateType = "_fi";
67
- continue;
68
- }
69
-
70
- if (currentPhase && currentGateType !== "_fi") {
71
- const itemMatch = line.match(/^-\s+\[([ x])\]\s+(.*)/);
72
- if (itemMatch) {
73
- currentPhase.items.push({
74
- checked: itemMatch[1] === "x",
75
- gate: currentGateType,
76
- });
77
- }
78
- }
79
- }
80
- if (currentPhase) phases.push(currentPhase);
81
- return phases;
51
+ const AUTHORABLE = new Set(["planned", "writable", ""]);
52
+ const CLOSES_PHASE = new Set(["passing", "skipped", "blocked"]);
53
+
54
+ const body = stripFrontmatter(content);
55
+ const sequence = phaseSequence(body);
56
+ const phases = parseImplPhases(content);
57
+ const { rows } = parseTrajectoryFromBody(body);
58
+ const hasTestPhases = sequence.some((p) => p.kind === "test");
59
+
60
+ const ordinalOf = (ref) => phaseOrdinal(ref, sequence);
61
+ const label = (p) =>
62
+ p.kind === "test"
63
+ ? `Test Phase ${p.number}`
64
+ : hasTestPhases
65
+ ? `Build Phase ${p.number}`
66
+ : `Phase ${p.number}`;
67
+
68
+ /** Rows the next phase must open by authoring, red. */
69
+ function writableAtNudge(next) {
70
+ const target = ordinalOf(next);
71
+ const due = rows.filter(
72
+ (r) =>
73
+ ordinalOf({ kind: r.writableAtKind, number: r.writableAt }) === target &&
74
+ AUTHORABLE.has(r.state),
75
+ );
76
+ if (due.length === 0) return null;
77
+ const lines = due.map((r) => ` [${r.id}] ${r.asserts}`);
78
+ return `${label(next)} opens with these tests to author (commit as failing before implementation work):\n${lines.join("\n")}`;
82
79
  }
83
80
 
84
- const phases = parsePhases(content);
85
-
86
- // Find the first phase that just became fully complete
87
- // (all items checked, including gates)
88
- for (const phase of phases) {
89
- const allChecked = phase.items.every((i) => i.checked);
90
- if (!allChecked) continue;
91
-
92
- // Check if the next phase has any unchecked items (meaning work hasn't started there yet)
93
- const nextPhase = phases.find((p) => p.number === phase.number + 1);
94
- if (nextPhase) {
95
- const nextHasUnchecked = nextPhase.items.some((i) => !i.checked);
96
- if (nextHasUnchecked) {
97
- // This phase is complete and next phase hasn't started
98
- const _result = {
99
- hookSpecificOutput: {
100
- hookEventName: "PostToolUse",
101
- },
102
- };
103
- console.error(
104
- `Phase ${phase.number} (${phase.name}) is fully complete. Call advance_plan to validate gates before starting Phase ${nextPhase.number}.`,
105
- );
106
- // Add trajectory nudges if applicable
107
- const trajectory = parseTrajectoryRows(content);
108
- if (trajectory.rows.length > 0) {
109
- const startNudge = writableAtNudge(trajectory, nextPhase.number);
110
- if (startNudge) console.error(`\n${startNudge}`);
111
- }
112
- process.exit(0);
113
- }
114
- }
81
+ const parts = [];
82
+
83
+ // A phase that just became fully complete, with the next one not yet started.
84
+ // "Not started" means NOTHING in it is checked — once the next phase has
85
+ // begun, repeating the send-off on every edit is noise, and the phase in
86
+ // progress needs its own blockers instead (falsification A19).
87
+ for (let i = 0; i + 1 < phases.length; i++) {
88
+ const phase = phases[i];
89
+ const next = phases[i + 1];
90
+ if (phase.items.length === 0 || !phase.items.every((x) => x.checked)) continue;
91
+ if (next.items.some((x) => x.checked)) continue;
92
+ parts.push(
93
+ `${label(phase)} (${phase.name}) is fully complete. Call advance_plan to validate gates before starting ${label(next)}.`,
94
+ );
95
+ const nudge = writableAtNudge(next);
96
+ if (nudge) parts.push(nudge);
97
+ break;
115
98
  }
116
99
 
117
- // Additional nudge: if any phase is mid-execution (some items checked, some not)
118
- // and has trajectory rows blocking close, warn about them.
119
- const trajectory = parseTrajectoryRows(content);
120
- if (trajectory.rows.length > 0) {
100
+ // Otherwise: a phase mid-flight whose trajectory rows will block its close.
101
+ if (parts.length === 0) {
121
102
  for (const phase of phases) {
122
- const anyChecked = phase.items.some((i) => i.checked);
123
- const allChecked = phase.items.every((i) => i.checked);
124
- if (!anyChecked || allChecked) continue;
125
- const blockers = trajectory.rows.filter(
126
- (row) =>
127
- row.passesAt === phase.number &&
128
- row.state !== "passing" &&
129
- row.state !== "skipped" &&
130
- row.state !== "blocked",
103
+ const checked = phase.items.filter((x) => x.checked).length;
104
+ if (checked === 0 || checked === phase.items.length) continue;
105
+ const target = ordinalOf(phase);
106
+ const blockers = rows.filter(
107
+ (r) =>
108
+ ordinalOf({ kind: r.passesAtKind, number: r.passesAt }) === target &&
109
+ !CLOSES_PHASE.has(r.state),
131
110
  );
132
- if (blockers.length > 0) {
133
- const lines = blockers.map((r) => ` [${r.id}] ${r.asserts} — state: ${r.state}`);
134
- console.error(
135
- `Phase ${phase.number} trajectory rows still not passing (will block phase close):\n${lines.join("\n")}`,
136
- );
137
- break; // one nudge per hook invocation
138
- }
139
- }
140
- }
141
-
142
- process.exit(0);
143
-
144
- // ------------------------------------------------------------------
145
- // Trajectory parsing (minimal — just id, passesAt, state)
146
- // ------------------------------------------------------------------
147
-
148
- function parseTrajectoryRows(implContent) {
149
- const fmMatch = implContent.match(/^---\n[\s\S]*?\n---\n/);
150
- const body = fmMatch ? implContent.slice(fmMatch[0].length) : implContent;
151
- const lines = body.split("\n");
152
-
153
- let inTrajectory = false;
154
- const tableLines = [];
155
- for (const line of lines) {
156
- if (/^##\s+Test Trajectory\b/.test(line)) {
157
- inTrajectory = true;
158
- continue;
159
- }
160
- if (!inTrajectory) continue;
161
- if (/^###\s+Deferred Verification\b/.test(line)) break;
162
- if (/^#{1,3}\s+/.test(line)) {
163
- const depth = (line.match(/^(#{1,6})/) || ["", ""])[1].length;
164
- if (depth <= 3) break;
165
- }
166
- tableLines.push(line);
167
- }
168
-
169
- const pipeLines = tableLines.filter((l) => l.trim().startsWith("|"));
170
- if (pipeLines.length < 2) return { rows: [] };
171
- const header = parseRow(pipeLines[0]);
172
- const sep = parseRow(pipeLines[1]);
173
- if (!sep.every((c) => /^:?-+:?$/.test(c))) return { rows: [] };
174
-
175
- const keys = header.map((h) => {
176
- const n = h.toLowerCase().trim();
177
- if (n === "id") return "id";
178
- if (n === "writable at") return "writableAt";
179
- if (n === "passes at") return "passesAt";
180
- if (n === "state") return "state";
181
- if (n === "asserts") return "asserts";
182
- return n;
183
- });
184
-
185
- const rows = [];
186
- for (let i = 2; i < pipeLines.length; i++) {
187
- const cells = parseRow(pipeLines[i]);
188
- if (cells.length !== keys.length) continue;
189
- const rec = {};
190
- for (let j = 0; j < keys.length; j++) rec[keys[j]] = cells[j];
191
- if (!rec.id) continue;
192
- const w = (rec.writableAt || "").match(/^\s*Phase\s+(\d+)\s*$/i);
193
- const p = (rec.passesAt || "").match(/^\s*Phase\s+(\d+)\s*$/i);
194
- rows.push({
195
- id: rec.id.trim(),
196
- asserts: (rec.asserts || "").trim(),
197
- writableAt: w ? Number.parseInt(w[1], 10) : Number.NaN,
198
- passesAt: p ? Number.parseInt(p[1], 10) : Number.NaN,
199
- state: (rec.state || "").toLowerCase().trim(),
200
- });
111
+ if (blockers.length === 0) continue;
112
+ const lines = blockers.map((r) => ` [${r.id}] ${r.asserts} — state: ${r.state}`);
113
+ parts.push(
114
+ `${label(phase)} trajectory rows still not passing (will block phase close):\n${lines.join("\n")}`,
115
+ );
116
+ break; // one nudge per invocation
201
117
  }
202
- return { rows };
203
118
  }
204
119
 
205
- function parseRow(line) {
206
- const t = line.trim();
207
- if (!t.startsWith("|") || !t.endsWith("|")) return [];
208
- return t
209
- .slice(1, -1)
210
- .split("|")
211
- .map((c) => c.trim());
212
- }
213
-
214
- function writableAtNudge(trajectory, phase) {
215
- const rows = trajectory.rows.filter(
216
- (r) =>
217
- r.writableAt === phase && (r.state === "planned" || r.state === "writable" || r.state === ""),
120
+ if (parts.length > 0) {
121
+ console.info(
122
+ JSON.stringify({
123
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: parts.join("\n\n") },
124
+ }),
218
125
  );
219
- if (rows.length === 0) return null;
220
- const lines = rows.map((r) => ` [${r.id}] ${r.asserts}`);
221
- return `Phase ${phase} opens with these tests to author (commit as failing before implementation work):\n${lines.join("\n")}`;
222
126
  }
127
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.44.0",
3
+ "version": "1.44.2",
4
4
  "description": "InDusk development system — skills, MCP tools, and CLI for structured AI-assisted development",
5
5
  "type": "module",
6
6
  "files": [
@@ -109,6 +109,6 @@
109
109
  "build": "tsc",
110
110
  "start": "node dist/server/index.js",
111
111
  "test": "vitest run",
112
- "release": "npm whoami && pnpm publish --no-git-checks"
112
+ "release": "bash scripts/release-guard.sh && npm whoami && pnpm publish --no-git-checks"
113
113
  }
114
114
  }
package/skills/cleanup.md CHANGED
@@ -39,7 +39,7 @@ Two consequences worth being explicit about:
39
39
 
40
40
  This is best-practice-guided decomposition, not blanket extraction. **Extraction is not universally good** — forcing it produces 8-line "components", prop-drilling, and the wrong abstraction (which costs more than the duplication it removes). Recommend only what best practices actually warrant.
41
41
 
42
- 1. **Find the changed files that deserve scrutiny.** The threshold is a focus tool, not a cap. Use `listOversizedChangedFiles(planRoot, baseRef)` — import it from `@infinitedusky/indusk-mcp/cleanup/oversized` in a consumer project (or `apps/indusk-mcp/src/lib/cleanup/oversized.ts` in the dusk monorepo), invoked via `tsx` or a short Node script — it diffs the plan's branch against its merge base and returns the changed files over their resolved cleanup cap, with `{ path, loc, cap, scope, isNew }`. The caps come from the `cleanup` config block in `.indusk/config.json` (`max_file_loc` + per-scope overrides). A file being flagged means "look here", not "this fails". **Workbench caveat:** run it against the git repo/worktree where the code lives — the workbench root (where `.indusk/` lives) is not a git repo, and the lib throws there rather than silently reporting nothing.
42
+ 1. **Find the changed files that deserve scrutiny.** The threshold is a focus tool, not a cap. Use `listOversizedChangedFiles(planRoot, baseRef)` — import it from `@infinitedusky/indusk-mcp/cleanup/oversized` in a consumer project (or `apps/indusk-mcp/src/lib/cleanup/oversized.ts` in the dusk monorepo), invoked via `tsx` or a short Node script — it diffs the plan's branch against its merge base and returns the changed files over their resolved cleanup cap, with `{ path, loc, cap, scope, isNew }`. The caps come from the `cleanup` config block in `.indusk/config.json` (`max_file_loc` + per-scope overrides). A file being flagged means "look here", not "this fails". **Workbench caveat:** run it against the git repo/worktree where the code lives. The workbench root (where `.indusk/` lives) holds the plan and none of the code, and the lib refuses there by declaration — it throws naming the declared repo dirs rather than reporting "nothing to clean" for code it never saw.
43
43
  2. **Apply the enabled domain extensions' best practices.** What counts as a cohesive unit is domain-specific — it comes from the enabled extensions, not from this skill. Read the skills of the project's enabled domain extensions (via `extensions_status` / `get_skill_summaries`):
44
44
  - **`nextjs`** — "minimize `"use client"` boundaries, push them as deep as possible"; "server components can't use hooks/event handlers". The concrete move on a Next.js project is often *pull the interactive `"use client"` island out of a big server component into its own file*, splitting server and client concerns.
45
45
  - **`react`** — "one component per file for non-trivial components".
@@ -206,6 +206,7 @@ Re-read CLAUDE.md in full. After the entire impl is done, verify:
206
206
  - **Key Decisions** — was the ADR decision added (post-ADR trigger)?
207
207
  - **Known Gotchas** — were all surprises and corrections captured?
208
208
  - **Current State** — does it reflect what's actually in progress?
209
+ - **Sequence** — does every active plan folder have exactly one fate: a step declared in the root `master.md` sequence, archived with a `closed_reason`, or standalone with a reason written in the master? A folder with none is how a close-out goes unwritten for weeks (indusk-makeover sat `completed` for 53 days in a queue labelled "any time"). Fix it here — declare it, archive it, or write the reason — and write any follow-on this plan surfaced into the brief of the step that owns it, not into this retrospective alone.
209
210
 
210
211
  Fix any inaccuracies. The impl may have changed things that weren't anticipated in the per-phase context updates.
211
212