wize-dev-kit 0.26.1 → 0.27.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 (69) hide show
  1. package/ARCH.md +2 -1
  2. package/CHANGELOG.md +64 -0
  3. package/README.es.md +28 -6
  4. package/README.md +28 -6
  5. package/README.pt-BR.md +28 -6
  6. package/adapters/continue/render.js +2 -3
  7. package/adapters/copilot/render.js +3 -3
  8. package/adapters/cursor/render.js +2 -3
  9. package/adapters/generic/render.js +2 -3
  10. package/adapters/opencode/render.js +3 -4
  11. package/adapters/windsurf/render.js +2 -3
  12. package/docs/WORKTREES.md +150 -0
  13. package/docs/WORKTREES.pt-BR.md +150 -0
  14. package/docs/harnesses/antigravity.md +2 -0
  15. package/docs/harnesses/antigravity.pt-BR.md +2 -0
  16. package/docs/harnesses/claude-code.md +2 -0
  17. package/docs/harnesses/claude-code.pt-BR.md +2 -0
  18. package/docs/harnesses/codex.md +3 -1
  19. package/docs/harnesses/codex.pt-BR.md +3 -1
  20. package/docs/harnesses/continue.md +2 -0
  21. package/docs/harnesses/continue.pt-BR.md +2 -0
  22. package/docs/harnesses/copilot.md +2 -0
  23. package/docs/harnesses/copilot.pt-BR.md +2 -0
  24. package/docs/harnesses/cursor.md +2 -0
  25. package/docs/harnesses/cursor.pt-BR.md +2 -0
  26. package/docs/harnesses/generic.md +2 -0
  27. package/docs/harnesses/generic.pt-BR.md +2 -0
  28. package/docs/harnesses/hermes.md +2 -0
  29. package/docs/harnesses/hermes.pt-BR.md +2 -0
  30. package/docs/harnesses/kimi-code.md +2 -0
  31. package/docs/harnesses/kimi-code.pt-BR.md +2 -0
  32. package/docs/harnesses/kiro.md +2 -0
  33. package/docs/harnesses/kiro.pt-BR.md +2 -0
  34. package/docs/harnesses/opencode.md +2 -0
  35. package/docs/harnesses/opencode.pt-BR.md +2 -0
  36. package/docs/harnesses/windsurf.md +2 -0
  37. package/docs/harnesses/windsurf.pt-BR.md +2 -0
  38. package/package.json +1 -1
  39. package/src/orchestrator-skills/module.yaml +1 -1
  40. package/src/orchestrator-skills/wize/skill.md +17 -7
  41. package/src/orchestrator-skills/wize-help/skill.md +20 -7
  42. package/src/orchestrator-skills/wize-update/skill.md +33 -27
  43. package/tools/installer/commands/doctor.js +186 -40
  44. package/tools/installer/commands/document-project.js +13 -3
  45. package/tools/installer/commands/hooks.js +215 -0
  46. package/tools/installer/commands/hydrate.js +298 -0
  47. package/tools/installer/commands/sync.js +50 -23
  48. package/tools/installer/commands/update.js +596 -120
  49. package/tools/installer/commands/version-check.js +69 -10
  50. package/tools/installer/document-project/modes/full-rescan.js +1 -1
  51. package/tools/installer/document-project/modes/initial-scan.js +5 -4
  52. package/tools/installer/document-project/modes/quick.js +11 -4
  53. package/tools/installer/git-context.js +219 -0
  54. package/tools/installer/git-ops.js +211 -0
  55. package/tools/installer/install-plan.js +136 -0
  56. package/tools/installer/marker.js +67 -0
  57. package/tools/installer/project-root.js +68 -0
  58. package/tools/installer/project-status.js +171 -0
  59. package/tools/installer/render-shared.js +170 -4
  60. package/tools/installer/render-targets.js +112 -0
  61. package/tools/installer/semver.js +59 -0
  62. package/tools/installer/setup-helpers.js +89 -31
  63. package/tools/installer/templates/hydrate.sh +163 -0
  64. package/tools/installer/templates/wize-kit-update.yml +38 -0
  65. package/tools/installer/toml.js +133 -0
  66. package/tools/installer/util.js +16 -0
  67. package/tools/installer/version-check.js +167 -37
  68. package/tools/installer/wize-cli.js +216 -78
  69. package/tools/installer/sync.js +0 -29
@@ -14,7 +14,12 @@ const fs = require('node:fs');
14
14
  const path = require('node:path');
15
15
  const { loadProjectConfig, loadInstalledKitVersion } = require('./update.js');
16
16
  const { detectHarnessCli } = require('../baseline.js');
17
- const { getLatestVersion, semverGreater } = require('../version-check.js');
17
+ const { getLatestNoWait } = require('../version-check.js');
18
+ const { compareVersions, semverGreater, safePin } = require('../semver.js');
19
+ const { gitContext, defaultBranch, trackedUnder } = require('../git-context.js');
20
+ const { getProjectStatus, supportsHydrate } = require('../project-status.js');
21
+ const { missingTargets } = require('../render-targets.js');
22
+ const { ALIAS_PATHS, gitignoreBlockStatus } = require('../setup-helpers.js');
18
23
 
19
24
  function fileExists(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
20
25
  function dirExists(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
@@ -72,12 +77,16 @@ function adapterTargetPath(targetCode, projectRoot) {
72
77
  }
73
78
  }
74
79
 
80
+ // Adapters that emit into more than one directory.
81
+ const EXTRA_ADAPTER_DIRS = { opencode: ['.opencode/commands'] };
82
+
75
83
  function countAdapterFiles(targetCode, projectRoot) {
76
84
  const dir = adapterTargetPath(targetCode, projectRoot);
77
- if (!dir || !dirExists(dir)) return 0;
85
+ if (!dir) return 0;
86
+ const dirs = [dir, ...(EXTRA_ADAPTER_DIRS[targetCode] || []).map(d => path.join(projectRoot, d))];
78
87
  // Be permissive on file extension; .mdc, .md, .prompt, and SKILL.md dirs all count.
79
88
  let count = 0;
80
- const stack = [dir];
89
+ const stack = dirs.filter(dirExists);
81
90
  while (stack.length) {
82
91
  const d = stack.pop();
83
92
  let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
@@ -103,7 +112,7 @@ function detectPhase(projectRoot) {
103
112
  listFilesUnder(path.join(projectRoot, '.wize/solutioning/stories'), /\.md$/).length === 0)
104
113
  return '3-solutioning (stories pending)';
105
114
  if (!has('.wize/implementation/tea/risk-profile.md')) return '3-closeout (risk profile pending)';
106
- if (!has('.wize/implementation/sprint-status.md')) return '4-implementation (sprint planning pending)';
115
+ if (!has('.wize/implementation/sprint-status.yaml') && !has('.wize/implementation/sprint-status.md')) return '4-implementation (sprint planning pending)';
107
116
  return '4-implementation';
108
117
  }
109
118
 
@@ -150,19 +159,39 @@ function knowledgeStatus(projectRoot) {
150
159
  return { exists: true, files: refreshed, pendingLines, toBeGeneratedMarkers, stateExists, stateAgeDays };
151
160
  }
152
161
 
162
+ function readHeadFromFs(gitDir, commonDir) {
163
+ const HEAD = readSafe(path.join(gitDir, 'HEAD')).trim();
164
+ if (!HEAD) return { branch: null, head: null, detached: false };
165
+ if (!HEAD.startsWith('ref: ')) return { branch: null, head: HEAD.slice(0, 7), detached: true };
166
+ const ref = HEAD.slice(5).trim();
167
+ const branch = ref.replace(/^refs\/heads\//, '');
168
+ let sha = null;
169
+ for (const dir of [gitDir, commonDir].filter(Boolean)) {
170
+ const loose = readSafe(path.join(dir, ref)).trim();
171
+ if (loose) { sha = loose; break; }
172
+ }
173
+ return { branch, head: sha ? sha.slice(0, 7) : null, detached: false };
174
+ }
175
+
176
+ // Uses git when it works; a `.git` file or directory is enough to count as a
177
+ // repository even when git itself cannot run (fake or broken `.git`).
153
178
  function gitInfo(projectRoot) {
154
- const dotGit = path.join(projectRoot, '.git');
155
- if (!dirExists(dotGit)) return { isRepo: false };
156
- let branch = '?'; let head = '?';
157
- try {
158
- const HEAD = readSafe(path.join(dotGit, 'HEAD')).trim();
159
- if (HEAD.startsWith('ref: ')) {
160
- branch = HEAD.slice(5).replace('refs/heads/', '');
161
- } else {
162
- head = HEAD.slice(0, 7);
163
- }
164
- } catch (_) {}
165
- return { isRepo: true, branch, head };
179
+ const ctx = gitContext(projectRoot);
180
+ if (!ctx.isRepo) return { isRepo: false };
181
+ let { branch, head, detached } = ctx;
182
+ if (!branch && !head && ctx.gitDir) {
183
+ const fromFs = readHeadFromFs(ctx.gitDir, ctx.commonDir);
184
+ branch = fromFs.branch; head = fromFs.head; detached = fromFs.detached;
185
+ }
186
+ return {
187
+ isRepo: true,
188
+ linked: !!ctx.linked,
189
+ primary: ctx.primary || null,
190
+ branch: branch || null,
191
+ detached: !!detached,
192
+ head: head || null,
193
+ defaultBranch: defaultBranch(projectRoot)
194
+ };
166
195
  }
167
196
 
168
197
  function severityIcon(level) {
@@ -170,7 +199,10 @@ function severityIcon(level) {
170
199
  }
171
200
 
172
201
  async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
173
- const log = opts.log || console.log;
202
+ const json = !!opts.json;
203
+ const env = opts.env || process.env;
204
+ const out = opts.log || console.log;
205
+ const log = (m) => { if (!json) out(m); };
174
206
  const cwd = projectRoot;
175
207
  const cfg = loadProjectConfig(cwd);
176
208
  const installed = loadInstalledKitVersion(kitRoot);
@@ -181,25 +213,39 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
181
213
  const targets = (cfg.install && cfg.install.ide_targets) || [];
182
214
  const profiles = (cfg.install && cfg.install.profiles) || [];
183
215
  const harnesses = detectHarnessCli({ preferIde: targets });
216
+ const pin = (cfg.project && cfg.project.kit_version) || null;
184
217
 
185
- // Try registry — best-effort, never blocks.
218
+ // Registry: cache only, never the network. WIZE_DISABLE_UPDATE_CHECK=1 skips it.
219
+ const updateCheckOff = env.WIZE_DISABLE_UPDATE_CHECK === '1';
186
220
  let registryLatest = null;
187
- try { registryLatest = await getLatestVersion(); } catch (_) {}
221
+ if (!updateCheckOff) {
222
+ // Without a terminal (an agent running `doctor --json`) no background refresh is started.
223
+ const isTTY = opts.isTTY !== undefined ? opts.isTTY : process.stdout.isTTY;
224
+ const lookupEnv = isTTY ? env : { ...env, WIZE_NO_BG_REFRESH: '1' };
225
+ try { registryLatest = (opts.getLatestNoWait || getLatestNoWait)({ env: lookupEnv }) || null; } catch (_) { /* best effort */ }
226
+ }
188
227
 
228
+ const status = getProjectStatus({ kitRoot, cwd, latest: registryLatest, latestSource: 'cache', env });
189
229
  const suggestions = [];
230
+ const suggest = (level, text, command = null) => suggestions.push(command ? { level, text, command } : { level, text });
190
231
 
191
232
  // -- Section: Kit --
192
233
  log('');
193
234
  log('Wize Dev Kit — Doctor');
194
235
  log('─────────────────────');
195
236
  log(`Kit version (installed): ${installed || '?'}`);
196
- log(`Kit version (project): ${cfg.project && cfg.project.kit_version || '— (no install)'}`);
197
- log(`Kit version (registry): ${registryLatest || '(offline or skipped)'}`);
198
- if (installed && cfg.project && cfg.project.kit_version && installed !== cfg.project.kit_version) {
199
- suggestions.push({ level: 'warn', text: `Project pinned to ${cfg.project.kit_version} but ${installed} is installed. Run \`npx wize-dev-kit update\` to refresh adapters.` });
237
+ log(`Kit version (project): ${pin || '— (no install)'}`);
238
+ log(`Kit version (registry): ${registryLatest || (updateCheckOff ? '(update check disabled)' : '(offline or skipped)')}`);
239
+ if (pin && installed) {
240
+ const cmp = compareVersions(pin, installed);
241
+ if (cmp < 0) {
242
+ suggest('warn', `Project pinned ${pin}, this kit is ${installed}. Run \`npx wize-dev-kit@latest update\` to converge.`, 'npx wize-dev-kit@latest update');
243
+ } else if (cmp > 0 && safePin(pin)) {
244
+ suggest('info', `Project pinned ${pin} is newer than this kit (${installed}). Use the pinned kit: \`npx --yes wize-dev-kit@${pin} doctor\`.`, `npx --yes wize-dev-kit@${pin} doctor`);
245
+ }
200
246
  }
201
247
  if (registryLatest && installed && semverGreater(registryLatest, installed)) {
202
- suggestions.push({ level: 'info', text: `Registry has ${registryLatest}; you're on ${installed}. Run \`npx wize-dev-kit@latest update\` to pick it up.` });
248
+ suggest('info', `Registry has ${registryLatest}; you're on ${installed}. Run \`npx wize-dev-kit@latest update\` to pick it up.`, 'npx wize-dev-kit@latest update');
203
249
  }
204
250
 
205
251
  // -- Section: Project --
@@ -207,8 +253,13 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
207
253
  log('Project');
208
254
  log('───────');
209
255
  if (!cfg.project) {
210
- log('No .wize/config/project.toml — kit is not installed here.');
211
- suggestions.push({ level: 'error', text: 'Run `npx wize-dev-kit install` to set the kit up in this repo.' });
256
+ if (status.reason === 'kit-not-on-branch') {
257
+ log(`No .wize/config/project.toml on this branch — the kit is installed on ${status.kit_at}'s branch.`);
258
+ suggest('warn', `The kit is installed on ${status.kit_at}'s branch but not on this one (kit-not-on-branch). Merge the kit into this branch, or pass --here to install here.`);
259
+ } else {
260
+ log('No .wize/config/project.toml — kit is not installed here.');
261
+ suggest('error', 'Run `npx wize-dev-kit install` to set the kit up in this repo.', 'npx wize-dev-kit install');
262
+ }
212
263
  } else {
213
264
  log(`Name: ${cfg.project.name || '?'}`);
214
265
  log(`Profiles: ${profiles.join(', ') || '(none)'}`);
@@ -218,7 +269,46 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
218
269
  log(`Current phase: ${phase}`);
219
270
  }
220
271
 
272
+ // -- Section: Hydration --
273
+ let hydration = null;
274
+ if (cfg.project) {
275
+ const missing = status.missing_targets;
276
+ hydration = {
277
+ status: status.hydrated,
278
+ marker: status.marker_version,
279
+ pin: status.pin,
280
+ binary: installed,
281
+ user_toml: status.user_toml,
282
+ missing_targets: missing,
283
+ command: status.commands.hydrate
284
+ };
285
+ log('');
286
+ log('Hydration');
287
+ log('─────────');
288
+ log(` status: ${status.hydrated}`);
289
+ log(` marker: ${status.marker_version || '(none)'}`);
290
+ log(` pin: ${status.pin || '(none)'}`);
291
+ log(` this kit: ${installed || '?'}`);
292
+ log(` user.toml: ${status.user_toml}`);
293
+ for (const t of targets) log(` ${t.padEnd(14)} ${missing.includes(t) ? '✖ not rendered' : '✓ rendered'}`);
294
+
295
+ const cmd = status.commands.hydrate;
296
+ if (status.hydrated === 'missing') {
297
+ suggest('error', `This checkout is not hydrated (not rendered: ${missing.join(', ')}). Run \`${cmd}\`.`, cmd);
298
+ } else if (status.hydrated === 'stale') {
299
+ suggest('error', `Rendered output is stale: marker says kit ${status.marker_version}, project pins ${status.pin}. Run \`${cmd}\`.`, cmd);
300
+ }
301
+ if (status.user_toml === 'missing' && status.hydrated === 'ok') {
302
+ if (supportsHydrate(status.pin)) {
303
+ suggest('error', `.wize/config/user.toml is missing in this checkout. Run \`${cmd}\`.`, cmd);
304
+ } else {
305
+ suggest('warn', '.wize/config/user.toml is missing in this checkout. Copy it from your primary checkout, or run `npx wize-dev-kit@latest update` to adopt hydrate.', 'npx wize-dev-kit@latest update');
306
+ }
307
+ }
308
+ }
309
+
221
310
  // -- Section: Adapters --
311
+ const adapters = [];
222
312
  if (cfg.install) {
223
313
  log('');
224
314
  log('IDE Adapters');
@@ -226,9 +316,33 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
226
316
  for (const t of targets) {
227
317
  const dir = adapterTargetPath(t, cwd);
228
318
  const count = countAdapterFiles(t, cwd);
229
- const status = count > 0 ? `✓ ${count} files` : '✖ none';
230
- log(` ${t.padEnd(14)} ${status.padEnd(15)} ${dir ? path.relative(cwd, dir) : '(no path)'}`);
231
- if (count === 0) suggestions.push({ level: 'warn', text: `Adapter "${t}" has no rendered files. Run \`npx wize-dev-kit sync\`.` });
319
+ const rendered = missingTargets(cwd, [t]).length === 0;
320
+ adapters.push({ target: t, files: count, rendered });
321
+ const label = rendered && count > 0 ? `✓ ${count} files` : count > 0 ? `✖ alias only` : '✖ none';
322
+ log(` ${t.padEnd(14)} ${label.padEnd(15)} ${dir ? path.relative(cwd, dir) : '(no path)'}`);
323
+ }
324
+ }
325
+
326
+ // -- Section: Leaks (generated files that git would see) --
327
+ const leaks = { tracked_aliases: [], gitignore: null };
328
+ if (cfg.project && git.isRepo) {
329
+ leaks.tracked_aliases = trackedUnder(cwd, ALIAS_PATHS);
330
+ const giFile = path.join(cwd, '.gitignore');
331
+ leaks.gitignore = gitignoreBlockStatus(fs.existsSync(giFile) ? readSafe(giFile) : null);
332
+ log('');
333
+ log('Generated files and git');
334
+ log('───────────────────────');
335
+ log(` tracked alias files: ${leaks.tracked_aliases.length}`);
336
+ log(` .gitignore block: ${leaks.gitignore}`);
337
+ const fix = 'npx wize-dev-kit@latest update';
338
+ if (leaks.tracked_aliases.length) {
339
+ const shown = leaks.tracked_aliases.slice(0, 3).join(', ') + (leaks.tracked_aliases.length > 3 ? ', ...' : '');
340
+ suggest('warn', `${leaks.tracked_aliases.length} generated alias file(s) are tracked by git (${shown}). Run \`${fix}\` to untrack them.`, fix);
341
+ }
342
+ if (leaks.gitignore === 'drift') {
343
+ suggest('warn', `The managed .gitignore block is out of date; generated files can leak into git status. Run \`${fix}\`.`, fix);
344
+ } else if (leaks.gitignore === 'missing') {
345
+ suggest('warn', `.gitignore has no wize-dev-kit managed block; generated files can leak into git status. Run \`${fix}\`.`, fix);
232
346
  }
233
347
  }
234
348
 
@@ -242,8 +356,8 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
242
356
  log(`CONCERNS: ${gates.CONCERNS}`);
243
357
  log(`FAIL: ${gates.FAIL}`);
244
358
  log(`WAIVED: ${gates.WAIVED}`);
245
- if (gates.FAIL > 0) suggestions.push({ level: 'error', text: `${gates.FAIL} story gate(s) at FAIL. Stories must not merge until resolved (advisory mode) or are blocking (enforcing mode).` });
246
- if (gates.CONCERNS > 0) suggestions.push({ level: 'warn', text: `${gates.CONCERNS} story gate(s) at CONCERNS. Review findings before next sprint.` });
359
+ if (gates.FAIL > 0) suggest('error', `${gates.FAIL} story gate(s) at FAIL. Stories must not merge until resolved (advisory mode) or are blocking (enforcing mode).`);
360
+ if (gates.CONCERNS > 0) suggest('warn', `${gates.CONCERNS} story gate(s) at CONCERNS. Review findings before next sprint.`);
247
361
  }
248
362
 
249
363
  // -- Section: Knowledge --
@@ -255,26 +369,26 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
255
369
  const age = f.days == null ? 'no last_refreshed' : `${f.days}d ago`;
256
370
  log(` ${f.name.padEnd(28)} ${age}`);
257
371
  if (f.days != null && f.days > 60) {
258
- suggestions.push({ level: 'warn', text: `\`${f.name}\` last refreshed ${f.days}d ago. Run \`/wize-refresh-knowledge\` after current sprint.` });
372
+ suggest('warn', `\`${f.name}\` last refreshed ${f.days}d ago. Run \`/wize-refresh-knowledge\` after current sprint.`);
259
373
  }
260
374
  }
261
375
  if (knowledge.pendingLines > 0) {
262
376
  log(` _pending.md: ${knowledge.pendingLines} inline note(s) waiting consolidation`);
263
377
  if (knowledge.pendingLines >= 5) {
264
- suggestions.push({ level: 'info', text: `${knowledge.pendingLines} notes piled up in _pending.md. Time to run \`/wize-refresh-knowledge\`.` });
378
+ suggest('info', `${knowledge.pendingLines} notes piled up in _pending.md. Time to run \`/wize-refresh-knowledge\`.`);
265
379
  }
266
380
  }
267
381
  const markerText = knowledge.toBeGeneratedMarkers > 0 ? `${knowledge.toBeGeneratedMarkers} marker(s)` : 'none';
268
382
  log(` To be generated markers: ${markerText}`);
269
383
  if (knowledge.toBeGeneratedMarkers >= 5) {
270
- suggestions.push({ level: 'warn', text: `${knowledge.toBeGeneratedMarkers} docs marked "To be generated" in index.md. Run \`wize-dev-kit document-project initial_scan deep\` to fill them.` });
384
+ suggest('warn', `${knowledge.toBeGeneratedMarkers} docs marked "To be generated" in index.md. Run \`wize-dev-kit document-project initial_scan deep\` to fill them.`);
271
385
  }
272
386
  const stateText = knowledge.stateExists
273
387
  ? (knowledge.stateAgeDays == null ? 'project-scan-report.json exists (age unknown)' : `project-scan-report.json ${knowledge.stateAgeDays}d old`)
274
388
  : 'no project-scan-report.json';
275
389
  log(` Scan state: ${stateText}`);
276
390
  if (!knowledge.stateExists) {
277
- suggestions.push({ level: 'info', text: 'No scan state file found. Run `wize-dev-kit document-project quick` to create a baseline + state.' });
391
+ suggest('info', 'No scan state file found. Run `wize-dev-kit document-project initial_scan` to create a baseline + state.');
278
392
  }
279
393
  }
280
394
 
@@ -284,7 +398,7 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
284
398
  log('───────────────────────');
285
399
  if (harnesses.length === 0) {
286
400
  log(' (none detected)');
287
- suggestions.push({ level: 'info', text: 'No harness CLI detected. Brownfield baseline + auto-run features only work in your IDE; the headless flow is unavailable.' });
401
+ suggest('info', 'No harness CLI detected. Brownfield baseline + auto-run features only work in your IDE; the headless flow is unavailable.');
288
402
  } else {
289
403
  for (const h of harnesses) log(` ${h.binary.padEnd(10)} ${h.path}`);
290
404
  }
@@ -295,10 +409,12 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
295
409
  log('───');
296
410
  if (!git.isRepo) {
297
411
  log(' (not a git repo)');
298
- suggestions.push({ level: 'warn', text: 'Not a git repository. Run `git init` before installing the kit so version history is recorded.' });
412
+ suggest('warn', 'Not a git repository. Run `git init` before installing the kit so version history is recorded.');
299
413
  } else {
300
- log(` branch: ${git.branch}`);
301
- log(` head: ${git.head}`);
414
+ log(` checkout: ${git.linked ? `linked worktree of ${git.primary || '?'}` : 'primary checkout'}`);
415
+ log(` branch: ${git.detached ? `detached @${git.head || '?'}` : (git.branch || '?')}`);
416
+ log(` head: ${git.head || '(no commits)'}`);
417
+ log(` default branch: ${git.defaultBranch || '(unknown)'}`);
302
418
  }
303
419
 
304
420
  // -- Section: Suggestions --
@@ -312,7 +428,37 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
312
428
  }
313
429
  log('');
314
430
 
315
- return { suggestions, phase, gates, knowledge, targets, profiles, installed, registryLatest };
431
+ const count = (level) => suggestions.filter(s => s.level === level).length;
432
+ const summary = { errors: count('error'), warnings: count('warn'), infos: count('info') };
433
+ const exitCode = opts.strict && summary.errors > 0 ? 1 : 0;
434
+
435
+ if (json) {
436
+ out(JSON.stringify({
437
+ schema: 1,
438
+ ok: summary.errors === 0,
439
+ exit_code: exitCode,
440
+ root: cwd,
441
+ kit: { installed, project: pin, registry: registryLatest, update_check: updateCheckOff ? 'disabled' : 'cache' },
442
+ project: cfg.project ? {
443
+ name: cfg.project.name || null,
444
+ profiles,
445
+ targets,
446
+ language: { communication: (cfg.language && cfg.language.communication) || null, document_output: (cfg.language && cfg.language.document_output) || null },
447
+ phase
448
+ } : null,
449
+ git,
450
+ hydration,
451
+ adapters,
452
+ leaks,
453
+ gates,
454
+ knowledge,
455
+ harnesses: harnesses.map(h => ({ binary: h.binary, path: h.path })),
456
+ suggestions,
457
+ summary
458
+ }));
459
+ }
460
+
461
+ return { suggestions, phase, gates, knowledge, targets, profiles, installed, registryLatest, status, hydration, leaks, git, summary, exitCode };
316
462
  }
317
463
 
318
464
  module.exports = {
@@ -21,6 +21,7 @@ function parseMode(args) {
21
21
  let resume = false;
22
22
  let scanLevel = 'quick';
23
23
  let target = null;
24
+ let overwrite = false;
24
25
 
25
26
  for (let i = 0; i < args.length; i++) {
26
27
  const arg = args[i];
@@ -28,6 +29,10 @@ function parseMode(args) {
28
29
  resume = true;
29
30
  continue;
30
31
  }
32
+ if (arg === '--force') {
33
+ overwrite = true;
34
+ continue;
35
+ }
31
36
  if (arg === '--target') {
32
37
  target = args[i + 1];
33
38
  i++;
@@ -53,7 +58,7 @@ function parseMode(args) {
53
58
  return { mode: null, error: `unknown mode: ${mode}. Valid modes: ${[...VALID_MODES].join(', ')}` };
54
59
  }
55
60
 
56
- return { mode, resume, scanLevel, target };
61
+ return { mode, resume, scanLevel, target, overwrite };
57
62
  }
58
63
 
59
64
  function cmdDocumentProject({ kitRoot, projectRoot, args = [], opts = {} }) {
@@ -66,12 +71,17 @@ function cmdDocumentProject({ kitRoot, projectRoot, args = [], opts = {} }) {
66
71
  }
67
72
 
68
73
  if (parsed.mode === 'quick') {
69
- const r = runQuick(projectRoot, { log });
74
+ const env = opts.env || process.env;
75
+ if (env.WIZE_SKIP_BASELINE === '1') {
76
+ log('WIZE_SKIP_BASELINE=1: skipped');
77
+ return { ok: true, mode: 'quick', changed: false, skipped: true, written: [] };
78
+ }
79
+ const r = runQuick(projectRoot, { log, overwrite: parsed.overwrite });
70
80
  return { ok: true, mode: 'quick', ...r };
71
81
  }
72
82
 
73
83
  if (parsed.mode === 'initial_scan') {
74
- const r = runInitialScan(projectRoot, { scanLevel: parsed.scanLevel, log });
84
+ const r = runInitialScan(projectRoot, { scanLevel: parsed.scanLevel, log, overwrite: parsed.overwrite });
75
85
  return { ok: true, mode: 'initial_scan', ...r };
76
86
  }
77
87
 
@@ -0,0 +1,215 @@
1
+ // `wize-dev-kit hooks` — opt-in automation that hydrates a checkout on its own.
2
+ //
3
+ // hooks install [--claude] [--cursor] [--github-action]
4
+ // hooks print <orca|claude|cursor|codex|github-action>
5
+ //
6
+ // `install` always writes the tracked, managed .wize/hooks/hydrate.sh and then
7
+ // only the config files whose flag was passed. `print` writes nothing. Nothing
8
+ // here runs without an explicit flag, because every file it writes is tracked.
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const HOOK_REL = '.wize/hooks/hydrate.sh';
16
+ const HOOK_HEADER = '# wize-dev-kit managed hook v1';
17
+ const TEMPLATES = path.resolve(__dirname, '..', 'templates');
18
+ const PRINT_TARGETS = ['orca', 'claude', 'cursor', 'codex', 'github-action'];
19
+
20
+ const readTemplate = (name) => fs.readFileSync(path.join(TEMPLATES, name), 'utf-8');
21
+
22
+ const CLAUDE_ENTRY = {
23
+ matcher: 'startup',
24
+ hooks: [{ type: 'command', command: 'sh "$CLAUDE_PROJECT_DIR"/.wize/hooks/hydrate.sh', timeout: 180 }]
25
+ };
26
+ const CURSOR_CONFIG = { 'setup-worktree': ['sh .wize/hooks/hydrate.sh'] };
27
+
28
+ // A hydrate.sh this kit wrote carries the header within its first lines. Any
29
+ // other file at that path belongs to the user.
30
+ function isManagedHook(content) {
31
+ return typeof content === 'string' && content.split('\n', 6).some(l => l.trim() === HOOK_HEADER);
32
+ }
33
+
34
+ // The new content for an installed hook, or null when it is absent, not
35
+ // managed, or already current. Used by `update` to refresh the script.
36
+ function refreshedHook(current) {
37
+ if (!isManagedHook(current)) return null;
38
+ const next = readTemplate('hydrate.sh');
39
+ return next === current ? null : next;
40
+ }
41
+
42
+ // Pure. `text` is the current .claude/settings.json (null when absent).
43
+ // -> { ok: true, changed, next } | { ok: false, error }. Every other key and
44
+ // hook is kept; an entry that already runs the managed script is not duplicated.
45
+ function mergeClaudeSettings(text) {
46
+ let settings = {};
47
+ if (text !== null && text.trim() !== '') {
48
+ try { settings = JSON.parse(text); }
49
+ catch (err) { return { ok: false, error: `.claude/settings.json is not valid JSON (${err.message})` }; }
50
+ }
51
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
52
+ return { ok: false, error: '.claude/settings.json must hold a JSON object' };
53
+ }
54
+ if (settings.hooks === undefined) settings.hooks = {};
55
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
56
+ return { ok: false, error: '.claude/settings.json: "hooks" must be an object' };
57
+ }
58
+ if (settings.hooks.SessionStart === undefined) settings.hooks.SessionStart = [];
59
+ if (!Array.isArray(settings.hooks.SessionStart)) {
60
+ return { ok: false, error: '.claude/settings.json: hooks.SessionStart must be an array' };
61
+ }
62
+ if (JSON.stringify(settings.hooks.SessionStart).includes(HOOK_REL)) return { ok: true, changed: false, next: text };
63
+ settings.hooks.SessionStart.push(CLAUDE_ENTRY);
64
+ return { ok: true, changed: true, next: JSON.stringify(settings, null, 2) + '\n' };
65
+ }
66
+
67
+ const cursorJson = () => JSON.stringify(CURSOR_CONFIG, null, 2) + '\n';
68
+
69
+ function printSnippet(target) {
70
+ const claude = JSON.stringify({ hooks: { SessionStart: [CLAUDE_ENTRY] } }, null, 2);
71
+ const snippets = {
72
+ orca: [
73
+ '# orca.yaml (commit it at the repo root)',
74
+ 'scripts:',
75
+ ` setup: sh ${HOOK_REL}`,
76
+ 'setupAgentStartupPolicy: wait-for-setup',
77
+ '',
78
+ '# A non-empty setup hook saved locally in Orca overrides orca.yaml. Clear it in the',
79
+ '# repository settings, or the line above never runs.',
80
+ '',
81
+ '# .worktreeinclude (gitignored files Orca copies into each new worktree)',
82
+ '.wize/config/user.toml'
83
+ ].join('\n'),
84
+ claude: [`# .claude/settings.json (or run: wize-dev-kit hooks install --claude)`, claude].join('\n'),
85
+ cursor: ['# .cursor/worktrees.json (or run: wize-dev-kit hooks install --cursor)', cursorJson().trimEnd()].join('\n'),
86
+ codex: [
87
+ 'Codex local environment: set the setup script for this project to',
88
+ '',
89
+ ` sh ${HOOK_REL}`,
90
+ '',
91
+ 'Codex stores it in its own local-environment config, so the kit does not write it.'
92
+ ].join('\n'),
93
+ 'github-action': [`# .github/workflows/wize-kit-update.yml (or run: wize-dev-kit hooks install --github-action)`, readTemplate('wize-kit-update.yml').trimEnd()].join('\n')
94
+ };
95
+ return snippets[target];
96
+ }
97
+
98
+ const USAGE = [
99
+ 'Usage:',
100
+ ' wize-dev-kit hooks install [--claude] [--cursor] [--github-action]',
101
+ ' wize-dev-kit hooks print <orca|claude|cursor|codex|github-action>',
102
+ '',
103
+ 'install always writes .wize/hooks/hydrate.sh; each flag adds one config file.'
104
+ ].join('\n');
105
+
106
+ function writeText(file, content, mode) {
107
+ fs.mkdirSync(path.dirname(file), { recursive: true });
108
+ fs.writeFileSync(file, content, mode ? { encoding: 'utf-8', mode } : 'utf-8');
109
+ }
110
+
111
+ function readOrNull(file) {
112
+ try { return fs.readFileSync(file, 'utf-8'); } catch { return null; }
113
+ }
114
+
115
+ function install({ projectRoot, flags, log, errlog }) {
116
+ const want = { claude: !!flags.claude, cursor: !!flags.cursor, action: !!(flags['github-action'] || flags.githubAction) };
117
+ if (!want.claude && !want.cursor && !want.action) {
118
+ errlog('✖ hooks install needs at least one of --claude, --cursor, --github-action');
119
+ errlog(USAGE);
120
+ return { exitCode: 1, written: [], unchanged: [] };
121
+ }
122
+
123
+ const written = [];
124
+ const unchanged = [];
125
+ const note = (rel, changed) => {
126
+ (changed ? written : unchanged).push(rel);
127
+ log(`${changed ? '✓ wrote' : '· unchanged'} ${rel}`);
128
+ };
129
+ const abs = (rel) => path.join(projectRoot, rel);
130
+ const fail = (message) => {
131
+ errlog(`✖ ${message}`);
132
+ return { exitCode: 1, written, unchanged };
133
+ };
134
+
135
+ // Validate everything that can refuse before writing anything.
136
+ const hookFile = abs(HOOK_REL);
137
+ const hookCurrent = readOrNull(hookFile);
138
+ if (hookCurrent !== null && !isManagedHook(hookCurrent)) {
139
+ return fail(`${HOOK_REL} exists and is not managed by wize-dev-kit; move it aside or merge by hand`);
140
+ }
141
+ let claudeMerge = null;
142
+ if (want.claude) {
143
+ claudeMerge = mergeClaudeSettings(readOrNull(abs('.claude/settings.json')));
144
+ if (!claudeMerge.ok) return fail(claudeMerge.error);
145
+ }
146
+ if (!fs.existsSync(abs('.wize/config/project.toml'))) {
147
+ errlog('warning: no .wize/config/project.toml here; the hook skips until the kit is installed');
148
+ }
149
+
150
+ const script = readTemplate('hydrate.sh');
151
+ if (hookCurrent !== script) writeText(hookFile, script);
152
+ fs.chmodSync(hookFile, 0o755);
153
+ note(HOOK_REL, hookCurrent !== script);
154
+
155
+ if (want.claude) {
156
+ if (claudeMerge.changed) writeText(abs('.claude/settings.json'), claudeMerge.next);
157
+ note('.claude/settings.json', claudeMerge.changed);
158
+ }
159
+
160
+ if (want.cursor) {
161
+ const rel = '.cursor/worktrees.json';
162
+ const current = readOrNull(abs(rel));
163
+ if (current === null) {
164
+ writeText(abs(rel), cursorJson());
165
+ note(rel, true);
166
+ } else if (current.includes(HOOK_REL)) {
167
+ note(rel, false);
168
+ } else {
169
+ unchanged.push(rel);
170
+ log(`· ${rel} exists; add this line to its "setup-worktree" array:`);
171
+ log(` "sh ${HOOK_REL}"`);
172
+ }
173
+ }
174
+
175
+ if (want.action) {
176
+ const rel = '.github/workflows/wize-kit-update.yml';
177
+ if (fs.existsSync(abs(rel))) note(rel, false);
178
+ else {
179
+ writeText(abs(rel), readTemplate('wize-kit-update.yml'));
180
+ note(rel, true);
181
+ log(' enable "Allow GitHub Actions to create and approve pull requests" in the repository settings');
182
+ }
183
+ }
184
+
185
+ return { exitCode: 0, written, unchanged };
186
+ }
187
+
188
+ function cmdHooks({ projectRoot, args = [], flags = {}, opts = {} } = {}) {
189
+ const log = opts.log || console.log;
190
+ const errlog = opts.errlog || console.error;
191
+ const [sub, target] = args;
192
+
193
+ if (sub === 'install') return install({ projectRoot, flags, log, errlog });
194
+ if (sub === 'print') {
195
+ if (!PRINT_TARGETS.includes(target)) {
196
+ errlog(`✖ ${target ? `unknown target "${target}"` : 'hooks print needs a target'}; available: ${PRINT_TARGETS.join(', ')}`);
197
+ return { exitCode: 1 };
198
+ }
199
+ log(printSnippet(target));
200
+ return { exitCode: 0 };
201
+ }
202
+ errlog(sub ? `✖ unknown hooks subcommand "${sub}"` : '✖ hooks needs a subcommand');
203
+ errlog(USAGE);
204
+ return { exitCode: 1 };
205
+ }
206
+
207
+ module.exports = {
208
+ cmdHooks,
209
+ mergeClaudeSettings,
210
+ isManagedHook,
211
+ refreshedHook,
212
+ HOOK_REL,
213
+ PRINT_TARGETS,
214
+ readTemplate
215
+ };