worclaude 2.6.2 → 2.7.0
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/CHANGELOG.md +50 -0
- package/SECURITY.md +3 -1
- package/package.json +1 -1
- package/src/commands/doctor.js +19 -3
- package/src/commands/init.js +8 -2
- package/src/commands/setup-state.js +31 -10
- package/src/commands/upgrade.js +57 -10
- package/src/core/merger.js +16 -17
- package/src/core/migration.js +4 -0
- package/src/core/scaffolder.js +32 -1
- package/src/core/setup-state.js +4 -1
- package/src/index.js +13 -2
- package/src/prompts/conflict-resolution.js +8 -1
- package/templates/commands/setup.md +278 -27
- package/templates/settings/base.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,56 @@ All notable changes to worclaude are documented in this file. Format loosely fol
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.7.0] — 2026-04-23
|
|
8
|
+
|
|
9
|
+
`/setup` hardening + UX revamp release. PR #115 closes 8 backend bug clusters surfaced by the v2.6.3 manual test matrix (upgrade-flow correctness, downgrade guard, doctor ghost detection, scaffolder exec bits, Commander routing). PR #116 fixes four `/setup` template failure modes — missing `schemaVersion` in SCAN state, `readme` object-shape mismatch in CONFIRM_MEDIUM, all-22-questions-asked despite detection, accept-off-topic-as-answer — and adds a `--from-file` flag to `worclaude setup-state save` plus `Bash(worclaude:*)` permissions so `/setup` runs without approval-prompt interruption. PR #117 adopts Claude Code's `AskUserQuestion` tool for 10 enumerable interview questions (arrow-key selection instead of free-text), redesigns CONFIRM prompts with a "Will be saved as" consequence sub-line and `?` / `help` command, and drops the 80-char readme truncation so users can read the full description before accepting. Dogfood-relevant: `.claude/commands/setup.md` auto-updates on `worclaude upgrade` to pick up the new template.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`worclaude setup-state save --from-file <path>`** (PR #116) — new CLI flag reading JSON state from a file path, mutually exclusive with `--stdin`. `/setup` now uses `Write` → `.claude/cache/setup-state.draft.json` → `--from-file` instead of heredoc-piped `--stdin`, eliminating the shell-interpolation safety prompts that interrupted every state transition.
|
|
14
|
+
- **`Bash(worclaude:*)` permission entries in `templates/settings/base.json`** (PR #116) — scoped for `worclaude`, `worclaude scan`, and `worclaude setup-state`. Freshly init'd projects run `/setup` end-to-end without approval prompts.
|
|
15
|
+
- **`/setup` Interaction mode contract** (PR #117) — four modes: `selectable` (`AskUserQuestion` single-choice), `multi-selectable` (`AskUserQuestion` multi-choice with `None` + `Other`), `hybrid` (detection pre-fill + accept/edit/replace), and `free-text`. Ten interview questions get non-default modes: `arch.classification`, `conventions.errors` / `logging` / `api_format`, `verification.staging` (selectable); `arch.external_apis`, `verification.required_checks` (multi-selectable); `features.core` / `nice_to_have` / `non_goals` (hybrid). Fallback to numbered-list parsing for Claude Code versions without `AskUserQuestion`.
|
|
16
|
+
- **`/setup` Field-help table** (PR #117) — single-source-of-truth reference listing all 14 detection fields + 22 questionIds with plain-English description, target output file/section, and example answer. Drives both the CONFIRM "→ Will be saved as: <target>" consequence line and the `?` / `help` command output.
|
|
17
|
+
- **`/setup` Detection-skip matrix** (PR #116) — auto-fills four questionIds with `"[auto-filled from <field>]"` when the scanner already answered them: `story.problem` (via readme), `arch.classification` (via monorepo), `arch.external_apis` (via externalApis), `workflow.new_dev_steps` (via scripts + readme).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`/setup` CONFIRM prompts** (PR #117) — `readme` render is no longer truncated to 80 chars + `…` (verbatim, 100-char soft-wrap); every detected item shows `→ Will be saved as: <target>` sub-line; `?` / `help` response produces the Field-help block without advancing state.
|
|
22
|
+
- **`/setup` INTERVIEW reply handling** (PR #116) — explicit classifier step with Answer / Skip / Cancel / Back / OFF-TOPIC buckets. OFF-TOPIC MUST restate, MUST NOT record, MUST NOT advance, MUST NOT persist. "Prefer off-topic when uncertain" guidance makes the rule less discretionary.
|
|
23
|
+
- **`/setup` SCAN state** (PR #116) — explicit `schemaVersion: 1` in a worked JSON example. Persist step uses `--from-file` with a `.claude/cache/setup-state.draft.json` staging path.
|
|
24
|
+
- **`/setup` CONFIRM_MEDIUM Storage rule** (PR #116) — `mediumResolved[field]` MUST be a string (never the raw `item.value` object). Applies to `readme` specifically, where the detector returns `{projectDescription, setupInstructions, fullPath}`.
|
|
25
|
+
- **`worclaude setup-state` validator error** (PR #116) — `mediumResolved.<field> must be a string` message now includes the received type and points at the CONFIRM_MEDIUM Storage rule.
|
|
26
|
+
- **`worclaude upgrade` preview** (PR #115) — new "Deleted (removed in current version)" section shows phantom hash entries (`categories.missingUntracked`) that the upgrade will prune. Previously silent.
|
|
27
|
+
- **`scaffoldAgentsMd` helper** (PR #115) — shared between `init.js scaffoldFresh` and `merger.js mergeAgentsMd`. Both call sites preserve pre-existing AGENTS.md and write the template alongside under `.claude/workflow-ref/AGENTS.md`.
|
|
28
|
+
- **`.claude/settings.json`** (PR #116, dogfood) — this repo's own install merges in `Bash(worclaude:*)` permissions so the dev-repo's Claude Code session doesn't hit approval prompts.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Legacy `*.workflow-ref.md` siblings stranded on version match** (PR #115) — `migrateWorkflowRefLocation` is now called before `upgrade.js`'s version-match early-exit and inside `runRepairOnlyFlow`. Projects with leftover siblings on the current version self-heal on the next `worclaude upgrade`.
|
|
33
|
+
- **Silent future-version downgrade via `worclaude upgrade --yes`** (PR #115) — new `semverGreaterThan` helper refuses downgrades with an actionable message (`Refusing to downgrade: installed vX.Y.Z is newer than CLI vA.B.C`) instead of rewriting meta to an older version with a success banner.
|
|
34
|
+
- **`doctor` missed ghost learnings files** (PR #115) — the `.claude/learnings/` check now warns on `.md` files present on disk but missing from `index.json`. Orphan detection (reverse direction) was already working.
|
|
35
|
+
- **`worclaude upgrade --yes` crashed on hook-conflict prompt** (PR #115) — discovered while scripting real-old-CLI upgrades during the v2.6.3 test cycle. `--yes` now threads into `promptHookConflict` and returns `'keep'` (safest default: never clobbers user customizations). Scripted upgrades across hook-template boundaries no longer require manual answers.
|
|
36
|
+
- **Fresh `worclaude init` silently overwrote existing AGENTS.md** (PR #115) — `scaffoldFresh` gates the write on `fileExists(AGENTS.md)` just like the merge path does. User-authored Cursor/Codex AGENTS.md is preserved byte-for-byte; template goes to `.claude/workflow-ref/AGENTS.md`.
|
|
37
|
+
- **Hook `.cjs` files had inconsistent exec bits post-scaffold** (PR #115) — `scaffoldHooks` now runs `fs.chmod(destPath, 0o755)` after each copy (POSIX-guarded). Previously dependent on template source mode; some files landed as `755`, some as `644`.
|
|
38
|
+
- **`worclaude setup-state <unknown>` exited with Commander's default code 1** (PR #115) — `setupState.on('command:*', ...)` listener emits the spec-matching `Error: unknown setup-state subcommand: <x> (expected one of show, save, reset, resume-info)` and sets `process.exitCode = 2`, aligning with the exit-2 convention `setup-state.js` already uses for bad arguments.
|
|
39
|
+
|
|
40
|
+
### Docs
|
|
41
|
+
|
|
42
|
+
- **`docs/spec/SPEC.md` `/setup` section** (this /sync) — rewritten to reflect the v2.7.0 surface: `--from-file` persistence path, Detection-skip matrix, Interaction mode contract, Field-help table, OFF-TOPIC classifier, `?` / `help` command, `AskUserQuestion` whitelisted at INTERVIEW states only.
|
|
43
|
+
- **`docs/spec/PROGRESS.md`** (this /sync) — new v2.6.3 and v2.7.0 release entries; Stats refreshed (729 → 782 tests, 53 → 57 test files).
|
|
44
|
+
|
|
45
|
+
## [2.6.3] — 2026-04-22
|
|
46
|
+
|
|
47
|
+
Second supply-chain scanner mirrored after Socket. Adds a `.snyk` policy file at the repo root with `exclude.global: [tests/fixtures/**]` so Snyk Open Source — whether invoked via the Snyk CLI, the `snyk/actions/node` GitHub Action, or any future integration — skips the intentionally-outdated fixture manifests under `tests/fixtures/scanner/**` that exist solely as deterministic inputs to the Part A project-scanner detectors (`next@14.2.3`, `vitest@1.4.0`, `prisma@5.10.0`, etc.). The fixtures are never installed (not referenced from root `package.json`), never shipped (excluded from the npm tarball by the `files` whitelist), and never executed. `SECURITY.md` is updated to name `.snyk` alongside `socket.yml` in the fixture-exclusion paragraph. The installed Snyk GitHub App only imports root `package.json` today, so the most immediate effect is keeping local `snyk test` runs honest; the file is also load-bearing for any future workflow that fails the build on high-severity findings. No runtime change for worclaude consumers.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`.snyk` policy file at repo root** (PR #112) — `version: v1.25.0` schema with `exclude.global: [tests/fixtures/**]` plus empty `ignore` and `patch` blocks. Mirrors the `socket.yml` pattern committed in v2.6.1 so Snyk Open Source treats the scanner fixtures the same way Socket does.
|
|
52
|
+
|
|
53
|
+
### Docs
|
|
54
|
+
|
|
55
|
+
- **`SECURITY.md` "Test fixture manifests are not real dependencies"** (PR #112) now names `.snyk` alongside `socket.yml` as the equivalent ignore directive for Snyk. The catch-all "Other SCA tools may need an equivalent `ignore` directive" sentence is preserved for any future scanner.
|
|
56
|
+
|
|
7
57
|
## [2.6.2] — 2026-04-22
|
|
8
58
|
|
|
9
59
|
Dev-dependency security bump. Adds an npm `overrides` entry pinning `brace-expansion` to `^1.1.13` to clear [GHSA-f886-m6hf-6m8v](https://github.com/advisories/GHSA-f886-m6hf-6m8v) — a moderate regex-DoS advisory against the 1.1.12 pulled transitively by `eslint → minimatch`. Post-override the lockfile resolves `brace-expansion@1.1.14` and `npm audit` drops from four moderate advisories to three. `SECURITY.md` is extended with a "Dev-only transitive advisories pending upstream fixes" section documenting the two remaining alerts ([GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) vite path traversal, [GHSA-67mh-4wv8-2f99](https://github.com/advisories/GHSA-67mh-4wv8-2f99) esbuild dev-server CORS) as upstream-blocked by the vitepress `1.6.4 → vite ^5 → esbuild ^0.21.3` chain — `npm overrides` cannot force esbuild past the vite peer contract, and no `vitepress@2.x` is on npm yet. Both advisories are dev-only (excluded from the published tarball by the `files` whitelist) and only reachable while a local dev server is running; tracked for upgrade in [issue #109](https://github.com/sefaertunc/Worclaude/issues/109). No runtime change for worclaude consumers.
|
package/SECURITY.md
CHANGED
|
@@ -42,7 +42,9 @@ These fixtures are:
|
|
|
42
42
|
dependency lists; it never imports or runs the packages named inside.
|
|
43
43
|
|
|
44
44
|
Worclaude's repo includes `socket.yml` to stop Socket from scanning this
|
|
45
|
-
directory
|
|
45
|
+
directory, and a `.snyk` policy file with an equivalent `exclude.global`
|
|
46
|
+
entry for Snyk Open Source. Other SCA tools may need an equivalent
|
|
47
|
+
`ignore` directive.
|
|
46
48
|
|
|
47
49
|
### Real runtime dependencies
|
|
48
50
|
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -770,11 +770,27 @@ async function checkLearnings(projectRoot) {
|
|
|
770
770
|
orphans.push(entry.file);
|
|
771
771
|
}
|
|
772
772
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
773
|
+
|
|
774
|
+
const indexedFiles = new Set(entries.map((e) => e?.file).filter(Boolean));
|
|
775
|
+
const diskFiles = (await fs.readdir(learningsDir)).filter(
|
|
776
|
+
(f) => f.endsWith('.md') && f !== '.gitkeep'
|
|
777
|
+
);
|
|
778
|
+
const ghosts = diskFiles.filter((f) => !indexedFiles.has(f));
|
|
779
|
+
|
|
780
|
+
const findings = [];
|
|
781
|
+
for (const f of orphans) {
|
|
782
|
+
findings.push(result(WARN, `Learnings entry: ${f}`, `Entry references missing file '${f}'`));
|
|
783
|
+
}
|
|
784
|
+
for (const f of ghosts) {
|
|
785
|
+
findings.push(
|
|
786
|
+
result(
|
|
787
|
+
WARN,
|
|
788
|
+
`Learnings ghost file: ${f}`,
|
|
789
|
+
`File exists but not referenced by index.json — run /learn to capture or delete manually`
|
|
790
|
+
)
|
|
776
791
|
);
|
|
777
792
|
}
|
|
793
|
+
if (findings.length > 0) return findings;
|
|
778
794
|
return [result(PASS, `Learnings: ${entries.length} entries captured`, null)];
|
|
779
795
|
}
|
|
780
796
|
|
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import {
|
|
5
5
|
scaffoldFile,
|
|
6
|
+
scaffoldAgentsMd,
|
|
6
7
|
updateGitignore,
|
|
7
8
|
scaffoldHooks,
|
|
8
9
|
scaffoldPluginJson,
|
|
@@ -297,8 +298,13 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
297
298
|
await scaffoldFile('core/claude-md.md', 'CLAUDE.md', variables, projectRoot);
|
|
298
299
|
spinner.text = 'Created CLAUDE.md';
|
|
299
300
|
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
// "Fresh" means no .claude/workflow-meta.json, but the user may still have
|
|
302
|
+
// an AGENTS.md from Cursor/Codex/another tool. The helper preserves it.
|
|
303
|
+
const agentsMdResult = await scaffoldAgentsMd(projectRoot, variables);
|
|
304
|
+
spinner.text =
|
|
305
|
+
agentsMdResult === 'preserved-with-ref'
|
|
306
|
+
? 'Preserved existing AGENTS.md (template saved to workflow-ref)'
|
|
307
|
+
: 'Created AGENTS.md';
|
|
302
308
|
|
|
303
309
|
await writeFile(path.join(projectRoot, '.claude', 'settings.json'), settingsStr);
|
|
304
310
|
spinner.text = 'Created .claude/settings.json';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { text } from 'node:stream/consumers';
|
|
3
4
|
import {
|
|
4
5
|
loadSetupState,
|
|
@@ -42,27 +43,47 @@ async function showSubcommand(projectRoot) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
async function saveSubcommand(projectRoot, options) {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const hasStdin = Boolean(options.stdin);
|
|
47
|
+
const hasFromFile = Boolean(options.fromFile);
|
|
48
|
+
|
|
49
|
+
if (hasStdin && hasFromFile) {
|
|
50
|
+
console.error('Error: --stdin and --from-file are mutually exclusive');
|
|
51
|
+
process.exitCode = 2;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!hasStdin && !hasFromFile) {
|
|
55
|
+
console.error('Error: save requires --stdin or --from-file <path>');
|
|
47
56
|
process.exitCode = 2;
|
|
48
57
|
return;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
const inputStream = options.inputStream || process.stdin;
|
|
52
60
|
let raw;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
if (hasFromFile) {
|
|
62
|
+
const filePath = path.resolve(options.fromFile);
|
|
63
|
+
try {
|
|
64
|
+
raw = await readFile(filePath, 'utf-8');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Error reading ${filePath}: ${err.message}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
const inputStream = options.inputStream || process.stdin;
|
|
72
|
+
try {
|
|
73
|
+
raw = await text(inputStream);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`Error reading stdin: ${err.message}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
let parsed;
|
|
62
82
|
try {
|
|
63
83
|
parsed = JSON.parse(raw);
|
|
64
84
|
} catch (err) {
|
|
65
|
-
|
|
85
|
+
const source = hasFromFile ? options.fromFile : 'stdin';
|
|
86
|
+
console.error(`Error: invalid JSON on ${source}: ${err.message}`);
|
|
66
87
|
process.exitCode = 1;
|
|
67
88
|
return;
|
|
68
89
|
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -21,6 +21,7 @@ import { getLatestNpmVersion } from '../utils/npm.js';
|
|
|
21
21
|
import * as display from '../utils/display.js';
|
|
22
22
|
import {
|
|
23
23
|
semverLessThan,
|
|
24
|
+
semverGreaterThan,
|
|
24
25
|
migrateSkillFormat,
|
|
25
26
|
patchAgentDescriptions,
|
|
26
27
|
migrateWorkflowRefLocation,
|
|
@@ -161,6 +162,13 @@ function renderTemplatePreview(categories, plan) {
|
|
|
161
162
|
}
|
|
162
163
|
display.newline();
|
|
163
164
|
}
|
|
165
|
+
if (categories.missingUntracked.length > 0) {
|
|
166
|
+
display.barLine(`${display.red('−')} Deleted (removed in current version):`);
|
|
167
|
+
for (const { key } of categories.missingUntracked) {
|
|
168
|
+
display.barLine(` ${display.red('−')} ${key}`);
|
|
169
|
+
}
|
|
170
|
+
display.newline();
|
|
171
|
+
}
|
|
164
172
|
if (categories.unchanged.length > 0) {
|
|
165
173
|
display.barLine(
|
|
166
174
|
`${display.dimColor('=')} Unchanged: ${display.dimColor(`${categories.unchanged.length} files`)}`
|
|
@@ -235,11 +243,25 @@ async function promptProceed(message) {
|
|
|
235
243
|
return proceed;
|
|
236
244
|
}
|
|
237
245
|
|
|
238
|
-
async function runRepairOnlyFlow({
|
|
246
|
+
async function runRepairOnlyFlow({
|
|
247
|
+
projectRoot,
|
|
248
|
+
meta,
|
|
249
|
+
plan,
|
|
250
|
+
variables,
|
|
251
|
+
dryRun,
|
|
252
|
+
yes,
|
|
253
|
+
refRelocationReport = { moved: 0, names: [], skipped: [] },
|
|
254
|
+
}) {
|
|
239
255
|
display.sectionHeader(`WORCLAUDE REPAIR (v${meta.version})`);
|
|
240
256
|
display.newline();
|
|
241
257
|
display.barLine('Drift detected:');
|
|
242
258
|
renderRepairPreview(plan);
|
|
259
|
+
if (refRelocationReport.moved > 0) {
|
|
260
|
+
display.barLine(
|
|
261
|
+
`${display.green('→')} Relocated ${refRelocationReport.moved} legacy ref file(s) ${display.dimColor('→ .claude/workflow-ref/')}`
|
|
262
|
+
);
|
|
263
|
+
display.newline();
|
|
264
|
+
}
|
|
243
265
|
|
|
244
266
|
if (dryRun) {
|
|
245
267
|
display.info('Dry run — no changes written.');
|
|
@@ -290,6 +312,11 @@ async function runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, y
|
|
|
290
312
|
if (result.sidecars.length > 0) {
|
|
291
313
|
display.barLine(`Sidecar: ${result.sidecars.length} suggestion files`);
|
|
292
314
|
}
|
|
315
|
+
if (refRelocationReport.moved > 0) {
|
|
316
|
+
display.barLine(
|
|
317
|
+
`Relocated: ${refRelocationReport.moved} legacy ref file(s) ${display.dimColor('→ .claude/workflow-ref/')}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
293
320
|
display.barLine(display.dimColor(`Backup: ${path.basename(backupDir)}/`));
|
|
294
321
|
|
|
295
322
|
if (result.migrationConflicts.length > 0 || result.sidecars.length > 0) {
|
|
@@ -352,16 +379,29 @@ export async function upgradeCommand(options = {}) {
|
|
|
352
379
|
const installedVersion = meta.version;
|
|
353
380
|
const versionMatch = installedVersion === currentVersion;
|
|
354
381
|
|
|
382
|
+
if (semverGreaterThan(installedVersion, currentVersion)) {
|
|
383
|
+
display.error(
|
|
384
|
+
`Refusing to downgrade: installed v${installedVersion} is newer than CLI v${currentVersion}.`
|
|
385
|
+
);
|
|
386
|
+
display.info('Upgrade the CLI with `npm install -g worclaude@latest`.');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// v2.5.1 migration: relocate legacy ref files. Runs before any early-exit
|
|
391
|
+
// so version-match projects with leftover legacy siblings still self-heal.
|
|
392
|
+
// Idempotent (skips already-migrated files).
|
|
393
|
+
const refRelocationReport = await migrateWorkflowRefLocation(projectRoot);
|
|
394
|
+
|
|
355
395
|
const categories = await categorizeFiles(projectRoot, meta);
|
|
356
396
|
const claudeMdContent = await readClaudeMd(projectRoot);
|
|
357
397
|
const plan = await buildRepairPlan(projectRoot, categories, claudeMdContent);
|
|
358
398
|
|
|
359
399
|
const repairWork = hasRepairWork(plan);
|
|
360
400
|
const templateWork = hasTemplateWork(categories, plan);
|
|
401
|
+
const refWork = refRelocationReport.moved > 0;
|
|
361
402
|
|
|
362
|
-
// Version match + no repair + no template work → up to date.
|
|
363
|
-
|
|
364
|
-
if (versionMatch && !repairWork && !templateWork) {
|
|
403
|
+
// Version match + no repair + no template work + no ref relocation → up to date.
|
|
404
|
+
if (versionMatch && !repairWork && !templateWork && !refWork) {
|
|
365
405
|
display.success(`Already up to date (v${currentVersion}).`);
|
|
366
406
|
return;
|
|
367
407
|
}
|
|
@@ -370,7 +410,15 @@ export async function upgradeCommand(options = {}) {
|
|
|
370
410
|
|
|
371
411
|
// Version match + repair only OR explicit --repair-only → repair-only flow
|
|
372
412
|
if ((versionMatch && !templateWork) || repairOnly) {
|
|
373
|
-
await runRepairOnlyFlow({
|
|
413
|
+
await runRepairOnlyFlow({
|
|
414
|
+
projectRoot,
|
|
415
|
+
meta,
|
|
416
|
+
plan,
|
|
417
|
+
variables,
|
|
418
|
+
dryRun,
|
|
419
|
+
yes,
|
|
420
|
+
refRelocationReport,
|
|
421
|
+
});
|
|
374
422
|
return;
|
|
375
423
|
}
|
|
376
424
|
|
|
@@ -439,10 +487,7 @@ export async function upgradeCommand(options = {}) {
|
|
|
439
487
|
spinner.start('Applying updates...');
|
|
440
488
|
}
|
|
441
489
|
|
|
442
|
-
// v2.5.1 migration
|
|
443
|
-
// dirs. Runs unconditionally since it's idempotent and the fix applies
|
|
444
|
-
// to any project with legacy refs, regardless of source version.
|
|
445
|
-
const refRelocationReport = await migrateWorkflowRefLocation(projectRoot);
|
|
490
|
+
// v2.5.1 migration already ran before the early-exit check (above).
|
|
446
491
|
if (refRelocationReport.moved > 0) {
|
|
447
492
|
spinner.text = `Relocated ${refRelocationReport.moved} legacy ref file(s)...`;
|
|
448
493
|
}
|
|
@@ -474,7 +519,9 @@ export async function upgradeCommand(options = {}) {
|
|
|
474
519
|
const useDocker = meta.useDocker || false;
|
|
475
520
|
const { settingsObject: workflowSettings } = await buildSettingsJson(techStack, useDocker);
|
|
476
521
|
const settingsReport = { added: { permissions: 0, hooks: 0 }, hookConflicts: [] };
|
|
477
|
-
await mergeSettingsPermissionsAndHooks(projectRoot, workflowSettings, settingsReport
|
|
522
|
+
await mergeSettingsPermissionsAndHooks(projectRoot, workflowSettings, settingsReport, {
|
|
523
|
+
yes,
|
|
524
|
+
});
|
|
478
525
|
spinner.text = 'Settings merged...';
|
|
479
526
|
}
|
|
480
527
|
|
package/src/core/merger.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
readTemplate,
|
|
6
6
|
substituteVariables,
|
|
7
7
|
scaffoldFile,
|
|
8
|
+
scaffoldAgentsMd,
|
|
8
9
|
mergeSettings,
|
|
9
10
|
scaffoldHooks,
|
|
10
11
|
scaffoldPluginJson,
|
|
@@ -230,7 +231,12 @@ async function mergeCommands(projectRoot, existingScan, report) {
|
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
export async function mergeSettingsPermissionsAndHooks(
|
|
234
|
+
export async function mergeSettingsPermissionsAndHooks(
|
|
235
|
+
projectRoot,
|
|
236
|
+
workflowSettings,
|
|
237
|
+
report,
|
|
238
|
+
options = {}
|
|
239
|
+
) {
|
|
234
240
|
const existingRaw = await readFile(path.join(projectRoot, '.claude', 'settings.json'));
|
|
235
241
|
const existing = parseUserJson(existingRaw, '.claude/settings.json');
|
|
236
242
|
|
|
@@ -277,8 +283,10 @@ export async function mergeSettingsPermissionsAndHooks(projectRoot, workflowSett
|
|
|
277
283
|
if (conflictCandidate) {
|
|
278
284
|
matched.add(conflictCandidate);
|
|
279
285
|
|
|
280
|
-
// Tier 3: conflict — ask user
|
|
281
|
-
const resolution = await promptHookConflict(category, conflictCandidate, workflowEntry
|
|
286
|
+
// Tier 3: conflict — ask user (or auto-keep if --yes)
|
|
287
|
+
const resolution = await promptHookConflict(category, conflictCandidate, workflowEntry, {
|
|
288
|
+
yes: options.yes,
|
|
289
|
+
});
|
|
282
290
|
|
|
283
291
|
if (resolution === 'replace') {
|
|
284
292
|
const idx = existingEntries.indexOf(conflictCandidate);
|
|
@@ -428,19 +436,10 @@ async function handleClaudeMd(projectRoot, existingScan, variables, selections,
|
|
|
428
436
|
}
|
|
429
437
|
}
|
|
430
438
|
|
|
431
|
-
async function mergeAgentsMd(projectRoot,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
} else {
|
|
436
|
-
await scaffoldFile(
|
|
437
|
-
'core/agents-md.md',
|
|
438
|
-
workflowRefRelPath('root/AGENTS.md'),
|
|
439
|
-
variables,
|
|
440
|
-
projectRoot
|
|
441
|
-
);
|
|
442
|
-
report.agentsMdHandling = 'saved-alongside';
|
|
443
|
-
}
|
|
439
|
+
async function mergeAgentsMd(projectRoot, variables, report) {
|
|
440
|
+
const result = await scaffoldAgentsMd(projectRoot, variables);
|
|
441
|
+
// Preserve the existing report-field vocabulary for downstream observability.
|
|
442
|
+
report.agentsMdHandling = result === 'created' ? 'created' : 'saved-alongside';
|
|
444
443
|
}
|
|
445
444
|
|
|
446
445
|
// --- Main merge function ---
|
|
@@ -498,7 +497,7 @@ export async function performMerge(
|
|
|
498
497
|
// Stop spinner before CLAUDE.md merge — interactive prompts for section selection
|
|
499
498
|
if (spinner) spinner.stop();
|
|
500
499
|
await handleClaudeMd(projectRoot, existingScan, variables, selections, report);
|
|
501
|
-
await mergeAgentsMd(projectRoot,
|
|
500
|
+
await mergeAgentsMd(projectRoot, variables, report);
|
|
502
501
|
if (spinner) spinner.start();
|
|
503
502
|
|
|
504
503
|
return report;
|
package/src/core/migration.js
CHANGED
package/src/core/scaffolder.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { fileExists, readFile, writeFile } from '../utils/file.js';
|
|
5
|
+
import { workflowRefRelPath } from './file-categorizer.js';
|
|
5
6
|
import { UNIVERSAL_AGENTS } from '../data/agents.js';
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -29,6 +30,25 @@ export async function scaffoldFile(templateRelativePath, destRelativePath, varia
|
|
|
29
30
|
await writeFile(destPath, content);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// Scaffold AGENTS.md at the project root, preserving any pre-existing file.
|
|
34
|
+
// When AGENTS.md exists (user-authored, Cursor/Codex/etc.), the template lands
|
|
35
|
+
// under .claude/workflow-ref/AGENTS.md so the user can diff + merge.
|
|
36
|
+
// Returns 'created' on fresh write or 'preserved-with-ref' when skipping.
|
|
37
|
+
export async function scaffoldAgentsMd(projectRoot, variables) {
|
|
38
|
+
const agentsMdPath = path.join(projectRoot, 'AGENTS.md');
|
|
39
|
+
if (await fileExists(agentsMdPath)) {
|
|
40
|
+
await scaffoldFile(
|
|
41
|
+
'core/agents-md.md',
|
|
42
|
+
workflowRefRelPath('root/AGENTS.md'),
|
|
43
|
+
variables,
|
|
44
|
+
projectRoot
|
|
45
|
+
);
|
|
46
|
+
return 'preserved-with-ref';
|
|
47
|
+
}
|
|
48
|
+
await scaffoldFile('core/agents-md.md', 'AGENTS.md', variables, projectRoot);
|
|
49
|
+
return 'created';
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
export async function scaffoldDirectory(templateDir, destDir, variables, projectRoot) {
|
|
33
53
|
const root = projectRoot || process.cwd();
|
|
34
54
|
const fs = await import('fs-extra');
|
|
@@ -98,10 +118,21 @@ export async function scaffoldHooks(projectRoot) {
|
|
|
98
118
|
const entries = await fs.readdir(hooksTemplateDir);
|
|
99
119
|
for (const entry of entries) {
|
|
100
120
|
if (!entry.endsWith('.cjs') && !entry.endsWith('.js')) continue;
|
|
101
|
-
|
|
121
|
+
const destPath = path.join(destDir, entry);
|
|
122
|
+
await fs.copy(path.join(hooksTemplateDir, entry), destPath, {
|
|
102
123
|
overwrite: false,
|
|
103
124
|
errorOnExist: false,
|
|
104
125
|
});
|
|
126
|
+
// Ensure consistent exec bits on POSIX. Windows has no chmod semantics
|
|
127
|
+
// worth enforcing; node+fs-extra no-ops there anyway, but guard to keep
|
|
128
|
+
// the intent obvious.
|
|
129
|
+
if (process.platform !== 'win32') {
|
|
130
|
+
try {
|
|
131
|
+
await fs.chmod(destPath, 0o755);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err.code !== 'ENOENT') throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
105
136
|
}
|
|
106
137
|
}
|
|
107
138
|
|
package/src/core/setup-state.js
CHANGED
|
@@ -114,7 +114,10 @@ function validateState(state) {
|
|
|
114
114
|
}
|
|
115
115
|
for (const [k, v] of Object.entries(state.mediumResolved)) {
|
|
116
116
|
if (typeof v !== 'string') {
|
|
117
|
-
throw new Error(
|
|
117
|
+
throw new Error(
|
|
118
|
+
`state.mediumResolved.${k} must be a string (got ${typeof v}; ` +
|
|
119
|
+
`stringify the rendered value per the CONFIRM_MEDIUM Storage rule)`
|
|
120
|
+
);
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
if (!state.interviewAnswers || typeof state.interviewAnswers !== 'object') {
|
package/src/index.js
CHANGED
|
@@ -84,8 +84,9 @@ setupState
|
|
|
84
84
|
|
|
85
85
|
setupState
|
|
86
86
|
.command('save')
|
|
87
|
-
.description('Read a JSON state from stdin, validate, and persist')
|
|
88
|
-
.option('--stdin', 'Read JSON from stdin
|
|
87
|
+
.description('Read a JSON state from stdin or a file, validate, and persist')
|
|
88
|
+
.option('--stdin', 'Read JSON from stdin')
|
|
89
|
+
.option('--from-file <path>', 'Read JSON from a file path')
|
|
89
90
|
.option('--path <dir>', 'Project root', process.cwd())
|
|
90
91
|
.action((options) => setupStateCommand('save', options));
|
|
91
92
|
|
|
@@ -101,4 +102,14 @@ setupState
|
|
|
101
102
|
.option('--path <dir>', 'Project root', process.cwd())
|
|
102
103
|
.action((options) => setupStateCommand('resume-info', options));
|
|
103
104
|
|
|
105
|
+
// Catch unknown setup-state subcommands with the spec-matching exit code 2.
|
|
106
|
+
// Commander's default would exit 1, but setup-state's own arg-error contract
|
|
107
|
+
// (see src/commands/setup-state.js) is exit 2 for bad inputs.
|
|
108
|
+
setupState.on('command:*', (operands) => {
|
|
109
|
+
console.error(
|
|
110
|
+
`Error: unknown setup-state subcommand: ${operands[0]} (expected one of show, save, reset, resume-info)`
|
|
111
|
+
);
|
|
112
|
+
process.exitCode = 2;
|
|
113
|
+
});
|
|
114
|
+
|
|
104
115
|
program.parse();
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import * as display from '../utils/display.js';
|
|
3
3
|
|
|
4
|
-
export async function promptHookConflict(hookCategory, existingHook, workflowHook) {
|
|
4
|
+
export async function promptHookConflict(hookCategory, existingHook, workflowHook, options = {}) {
|
|
5
5
|
display.newline();
|
|
6
6
|
display.warn(`Hook conflict: ${hookCategory} matcher "${existingHook.matcher}"`);
|
|
7
7
|
display.dim(` Existing: ${existingHook.hooks[0].command}`);
|
|
8
8
|
display.dim(` Workflow: ${workflowHook.hooks[0].command}`);
|
|
9
9
|
|
|
10
|
+
// Non-interactive: preserve the user's existing hook. Safer than "replace"
|
|
11
|
+
// during scripted upgrades — never silently clobbers customizations.
|
|
12
|
+
if (options.yes) {
|
|
13
|
+
display.dim(' --yes: kept existing hook (non-interactive default).');
|
|
14
|
+
return 'keep';
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
const { resolution } = await inquirer.prompt([
|
|
11
18
|
{
|
|
12
19
|
type: 'list',
|
|
@@ -54,12 +54,22 @@ normally apply.
|
|
|
54
54
|
|
|
55
55
|
- Shell: `worclaude scan --path .` (SCAN only)
|
|
56
56
|
- Shell: `worclaude setup-state show --path .`
|
|
57
|
-
- Shell: `worclaude setup-state save --
|
|
58
|
-
|
|
57
|
+
- Shell: `worclaude setup-state save --from-file .claude/cache/setup-state.draft.json --path .`
|
|
58
|
+
— the ONLY way state is persisted. Use `Write` to produce the
|
|
59
|
+
draft JSON first, then invoke the CLI against that path. This
|
|
60
|
+
avoids Claude Code's shell-interpolation safety layer that
|
|
61
|
+
triggers on heredoc-with-variable-expansion patterns.
|
|
59
62
|
- Shell: `worclaude setup-state reset --path .`
|
|
60
63
|
- Shell: `worclaude setup-state resume-info --path .`
|
|
61
64
|
- Read: `.claude/cache/detection-report.json`
|
|
62
65
|
- Read: `.claude/cache/setup-state.json`
|
|
66
|
+
- Write: `.claude/cache/setup-state.draft.json` (state-save staging
|
|
67
|
+
only — overwritten each save).
|
|
68
|
+
- Tool: `AskUserQuestion` (INTERVIEW states only, per the
|
|
69
|
+
Interaction mode contract — `selectable` / `multi-selectable`
|
|
70
|
+
questions). Not permitted at CONFIRM_HIGH / CONFIRM_MEDIUM:
|
|
71
|
+
those render VERBATIM per rule #7 so the text-parser contract
|
|
72
|
+
stays stable.
|
|
63
73
|
|
|
64
74
|
At WRITE state the whitelist RELAXES to additionally permit:
|
|
65
75
|
|
|
@@ -183,10 +193,27 @@ ENTRY:
|
|
|
183
193
|
- ...
|
|
184
194
|
```
|
|
185
195
|
|
|
186
|
-
- State file mutation: write a fresh state with
|
|
187
|
-
`
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
- State file mutation: write a fresh state with EXACTLY these fields.
|
|
197
|
+
`schemaVersion: 1` is REQUIRED — the validator rejects anything else
|
|
198
|
+
with `Unsupported schemaVersion: undefined`.
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"schemaVersion": 1,
|
|
203
|
+
"currentState": "SCAN",
|
|
204
|
+
"startedAt": "<ISO timestamp, now>",
|
|
205
|
+
"updatedAt": "<ISO timestamp, now>",
|
|
206
|
+
"detectionReportPath": ".claude/cache/detection-report.json",
|
|
207
|
+
"highConfirmedAccepted": [],
|
|
208
|
+
"highConfirmedRejected": [],
|
|
209
|
+
"mediumResolved": {},
|
|
210
|
+
"interviewAnswers": {}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Persist via `Write` → `.claude/cache/setup-state.draft.json` →
|
|
215
|
+
`worclaude setup-state save --from-file .claude/cache/setup-state.draft.json --path .`
|
|
216
|
+
(see rule #5).
|
|
190
217
|
|
|
191
218
|
EXIT: advance to CONFIRM_HIGH.
|
|
192
219
|
|
|
@@ -201,21 +228,26 @@ ENTRY:
|
|
|
201
228
|
`highConfirmedRejected: []`, and transition to CONFIRM_MEDIUM
|
|
202
229
|
(trivial exit per rule #1).
|
|
203
230
|
- Otherwise render VERBATIM (wrapped in a triple-backtick fenced code
|
|
204
|
-
block):
|
|
231
|
+
block). Below every item add a dim "→ Will be saved as: `<target>`"
|
|
232
|
+
line pulled from the **Field-help table** so the user knows what
|
|
233
|
+
accepting means:
|
|
205
234
|
|
|
206
235
|
```
|
|
207
236
|
I scanned your project. Please confirm the high-confidence
|
|
208
237
|
detections below. Reply with the numbers of any items that are
|
|
209
|
-
WRONG (e.g., "2, 5"), or reply "ok" to accept all.
|
|
238
|
+
WRONG (e.g., "2, 5"), or reply "ok" to accept all, or "?" for help.
|
|
210
239
|
|
|
211
240
|
[x] 1. <formatField(item1.field)>: <renderValue(item1)> (from <item1.source>)
|
|
241
|
+
→ Will be saved as: <target1>
|
|
212
242
|
[x] 2. ...
|
|
243
|
+
→ Will be saved as: <target2>
|
|
213
244
|
|
|
214
245
|
Your response:
|
|
215
246
|
```
|
|
216
247
|
|
|
217
248
|
`<formatField>` and `<renderValue>` are defined in the **Field
|
|
218
|
-
rendering table** below.
|
|
249
|
+
rendering table** below. `<target>` is pulled from the **Field-help
|
|
250
|
+
table** (also below) and must be included verbatim per the table.
|
|
219
251
|
|
|
220
252
|
Response parsing (case-insensitive, whitespace trimmed):
|
|
221
253
|
|
|
@@ -225,12 +257,18 @@ Response parsing (case-insensitive, whitespace trimmed):
|
|
|
225
257
|
- One or more integers (comma or space separated) in range 1..N →
|
|
226
258
|
those items are rejected; split fields into accepted/rejected
|
|
227
259
|
accordingly (in rendered order).
|
|
260
|
+
- `?` | `help` → render the **Field-help** block for EACH displayed
|
|
261
|
+
field (description + target + example) without advancing state. Then
|
|
262
|
+
restate the prompt above (same text, same items). Do NOT persist a
|
|
263
|
+
mutation for the help render.
|
|
228
264
|
- Anything else (including integers out of range) → rule #3 fires:
|
|
229
|
-
restate with "I need either 'ok'
|
|
230
|
-
the items above (e.g., '2, 5'). To cancel, type
|
|
265
|
+
restate with "I need either 'ok', numbers from 1 to `<N>` matching
|
|
266
|
+
the items above (e.g., '2, 5'), or `?` for help. To cancel, type
|
|
267
|
+
`cancel setup`."
|
|
231
268
|
|
|
232
269
|
State file mutation: persist the updated arrays via
|
|
233
|
-
`worclaude setup-state save --
|
|
270
|
+
`worclaude setup-state save --from-file .claude/cache/setup-state.draft.json --path .`
|
|
271
|
+
(see rule #5).
|
|
234
272
|
|
|
235
273
|
EXIT: advance to CONFIRM_MEDIUM.
|
|
236
274
|
|
|
@@ -252,9 +290,10 @@ ENTRY:
|
|
|
252
290
|
<formatField(field)> (detected from <source>):
|
|
253
291
|
|
|
254
292
|
1. <renderValue(item)>
|
|
293
|
+
→ Will be saved as: <target>
|
|
255
294
|
2. Other (I'll type my own)
|
|
256
295
|
|
|
257
|
-
Reply with the number of your choice (default: 1):
|
|
296
|
+
Reply with the number of your choice (default: 1), or `?` for help:
|
|
258
297
|
```
|
|
259
298
|
|
|
260
299
|
**Shape B — `candidates` is a non-empty array** (emitted by
|
|
@@ -262,6 +301,7 @@ ENTRY:
|
|
|
262
301
|
|
|
263
302
|
```
|
|
264
303
|
<formatField(field)> (detected from <source>):
|
|
304
|
+
→ Will be saved as: <target>
|
|
265
305
|
|
|
266
306
|
1. <candidates[0]>
|
|
267
307
|
2. <candidates[1]>
|
|
@@ -269,21 +309,39 @@ ENTRY:
|
|
|
269
309
|
N. <candidates[N-1]>
|
|
270
310
|
N+1. Other (I'll type my own)
|
|
271
311
|
|
|
272
|
-
Reply with the number of your choice (default: 1):
|
|
312
|
+
Reply with the number of your choice (default: 1), or `?` for help:
|
|
273
313
|
```
|
|
274
314
|
|
|
315
|
+
`<target>` comes from the **Field-help table** below. Render it
|
|
316
|
+
verbatim per the table.
|
|
317
|
+
|
|
275
318
|
`candidates[0]` equals `item.value` — default-1 accepts the detected
|
|
276
319
|
value.
|
|
277
320
|
|
|
321
|
+
**Storage rule:** `mediumResolved[field]` MUST be a string. Store the
|
|
322
|
+
exact `renderValue(item)` that was shown to the user on option 1, or
|
|
323
|
+
the user's trimmed free-text on "Other", or `candidates[k-1]` (shape
|
|
324
|
+
B) for a numbered pick. **NEVER store the raw `item.value` object** —
|
|
325
|
+
for fields like `readme` whose detected value is an object
|
|
326
|
+
(`{projectDescription, setupInstructions, fullPath}`) the validator
|
|
327
|
+
will reject the mutation with `state.mediumResolved.<field> must be a
|
|
328
|
+
string`.
|
|
329
|
+
|
|
278
330
|
Response parsing (per item):
|
|
279
331
|
|
|
280
|
-
- `""` | `1` | `default` → accept item 1 (
|
|
332
|
+
- `""` | `1` | `default` → accept item 1; store `renderValue(item)`
|
|
333
|
+
as a string per the Storage rule above.
|
|
281
334
|
- The final "Other" number (`2` in shape A, `N+1` in shape B) →
|
|
282
335
|
follow-up free-text prompt: "Go ahead — what's the value you'd like
|
|
283
|
-
to use?".
|
|
284
|
-
- Integer in range `2..N` (shape B only) →
|
|
285
|
-
-
|
|
286
|
-
|
|
336
|
+
to use?". Store the trimmed reply.
|
|
337
|
+
- Integer in range `2..N` (shape B only) → store `candidates[k-1]`.
|
|
338
|
+
- `?` | `help` → render the **Field-help** block for this field
|
|
339
|
+
(description + target + example) without advancing state. Then
|
|
340
|
+
restate the prompt above. Do NOT persist a mutation for the help
|
|
341
|
+
render.
|
|
342
|
+
- Anything else → restate with "I need a number from 1 to `<max>`,
|
|
343
|
+
empty for the default, or `?` for help. To cancel, type
|
|
344
|
+
`cancel setup`."
|
|
287
345
|
|
|
288
346
|
State file mutation: after EACH item is resolved (not batched),
|
|
289
347
|
append to `mediumResolved` and persist.
|
|
@@ -298,14 +356,49 @@ Shared ENTRY protocol for each INTERVIEW state:
|
|
|
298
356
|
**QuestionId enumeration** below plus any rejected fields routed to
|
|
299
357
|
this state from CONFIRM_HIGH (per the **Rejected-field re-ask
|
|
300
358
|
routing** table).
|
|
301
|
-
-
|
|
359
|
+
- Apply the **Detection-skip matrix** (below the enumeration): for
|
|
360
|
+
each `questionId` whose skip-field is present in
|
|
361
|
+
`highConfirmedAccepted`, record
|
|
362
|
+
`interviewAnswers[<questionId>] = "[auto-filled from <field>]"` and
|
|
363
|
+
persist (same Storage rule applies — the value is always a string)
|
|
364
|
+
BEFORE evaluating the already-answered skip-list.
|
|
365
|
+
- Skip any `questionId` already present in `interviewAnswers`
|
|
366
|
+
(including auto-filled ones).
|
|
302
367
|
- Resume preamble (only if ANY questionId is already answered AND at
|
|
303
368
|
least one remains): "Resuming `<STATE_NAME>`. Already have:
|
|
304
369
|
`<comma-list>`. Next: `<next questionId>`."
|
|
305
370
|
- If ALL `questionId`s for this state are present, trivially-exit:
|
|
306
371
|
persist a state update (only `currentState` and `updatedAt` change)
|
|
307
372
|
and advance.
|
|
308
|
-
- Ask remaining questions
|
|
373
|
+
- Ask remaining questions in enumeration order. For each question,
|
|
374
|
+
look up its `interactionMode` in the **Per-question interaction
|
|
375
|
+
table** below and use the matching tool:
|
|
376
|
+
- `selectable` / `multi-selectable` → `AskUserQuestion`
|
|
377
|
+
- `hybrid` → pre-fill from the listed detection source, then offer
|
|
378
|
+
accept / edit / replace
|
|
379
|
+
- `free-text` → ordinary text prompt
|
|
380
|
+
The Storage rule (`interviewAnswers[<questionId>]` must be a string)
|
|
381
|
+
applies uniformly — no object shapes regardless of mode.
|
|
382
|
+
- **Reply classification** — before recording a reply as
|
|
383
|
+
`interviewAnswers[<questionId>]`, classify it:
|
|
384
|
+
- **Answer**: a response that plausibly fits the semantic scope of
|
|
385
|
+
`<questionId>` (e.g., `arch.classification` expects one of the
|
|
386
|
+
enum values or a short phrase about system shape; `story.audience`
|
|
387
|
+
expects a description of people/roles). Record and advance.
|
|
388
|
+
- **Skip trigger**: `skip` or `skip all` — rules below apply.
|
|
389
|
+
- **Cancel trigger**: matches rule #4's regex — rule #4 applies.
|
|
390
|
+
- **Back trigger**: starts with `back` — rule below applies.
|
|
391
|
+
- **Everything else → OFF-TOPIC.** Apply rule #3: restate the
|
|
392
|
+
pending question with the off-topic prefix. You MUST NOT record
|
|
393
|
+
the reply in `interviewAnswers`. You MUST NOT advance
|
|
394
|
+
`currentState` or the question pointer. Do NOT persist a mutation
|
|
395
|
+
for this exchange. A topic-mismatched reply is not an answer just
|
|
396
|
+
because it is a sentence — if the reply would land in a different
|
|
397
|
+
section of SPEC.md than the one tied to this `<questionId>`, it is
|
|
398
|
+
off-topic.
|
|
399
|
+
|
|
400
|
+
Prefer off-topic when uncertain. An unnecessary restate costs one
|
|
401
|
+
turn; a mis-filed answer corrupts the state file.
|
|
309
402
|
- `skip` on a question → record `interviewAnswers[<questionId>] =
|
|
310
403
|
"[skipped]"`, advance to the next question in this state.
|
|
311
404
|
- `skip all` → record every remaining `questionId` as `[skipped]`,
|
|
@@ -313,13 +406,11 @@ Shared ENTRY protocol for each INTERVIEW state:
|
|
|
313
406
|
- `back` → restate the current question with the prefix "I can't go
|
|
314
407
|
back within a single setup run. Finish this run and edit the output
|
|
315
408
|
files afterward." (rule #2).
|
|
316
|
-
- Rule #3 applies within interview states: off-topic replies trigger
|
|
317
|
-
a restatement of the pending question, not an answer to the
|
|
318
|
-
off-topic question.
|
|
319
409
|
|
|
320
410
|
State file mutation: after EACH question is answered or skipped,
|
|
321
|
-
persist via `worclaude setup-state save --
|
|
322
|
-
next prompt. Resume granularity is
|
|
411
|
+
persist via `worclaude setup-state save --from-file .claude/cache/setup-state.draft.json --path .`
|
|
412
|
+
(see rule #5) BEFORE rendering the next prompt. Resume granularity is
|
|
413
|
+
per-question.
|
|
323
414
|
|
|
324
415
|
EXIT: advance to the next state; INTERVIEW_VERIFICATION exits to
|
|
325
416
|
WRITE.
|
|
@@ -430,6 +521,78 @@ detection):
|
|
|
430
521
|
- `verification.staging` — staging/preview environment.
|
|
431
522
|
- `verification.required_checks` — CI required checks.
|
|
432
523
|
|
|
524
|
+
### Interaction mode
|
|
525
|
+
|
|
526
|
+
Each interview question is asked in ONE of four modes. The mode lives
|
|
527
|
+
next to the question in the table below. This controls which tool you
|
|
528
|
+
use to collect the answer — text input, menu, checklist, or hybrid.
|
|
529
|
+
|
|
530
|
+
- `selectable` — invoke the `AskUserQuestion` tool with the listed
|
|
531
|
+
choices, `multiSelect: false`. The user gets arrow-key navigation.
|
|
532
|
+
If the user picks "Other (I'll type my own)", follow up with a
|
|
533
|
+
free-text prompt: "Go ahead — what's the value you'd like to use?".
|
|
534
|
+
- `multi-selectable` — invoke `AskUserQuestion` with `multiSelect:
|
|
535
|
+
true` and the listed choices. Always prepend `None` as the first
|
|
536
|
+
choice and append "Other (I'll type my own)" as the last. On
|
|
537
|
+
"Other", follow up with free-text. The stored value joins selections
|
|
538
|
+
with `, ` (e.g. `"REST, GraphQL"`). Selecting `None` alone stores
|
|
539
|
+
`"none"`.
|
|
540
|
+
- `hybrid` — pre-fill a bullet list from detection (see the question
|
|
541
|
+
row for which detection field feeds it) and ask: "I pre-filled this
|
|
542
|
+
from your README/scan. Accept, edit, or replace?". `accept` stores
|
|
543
|
+
the pre-filled text verbatim; `edit` offers free-text with the
|
|
544
|
+
pre-fill visible; `replace` starts from empty.
|
|
545
|
+
- `free-text` — ordinary text prompt (default). No `AskUserQuestion`.
|
|
546
|
+
|
|
547
|
+
Regardless of mode, `interviewAnswers[<questionId>]` MUST be a string
|
|
548
|
+
per the Storage rule. For `multi-selectable`, join with `, `. For
|
|
549
|
+
`hybrid`, store the final accepted or edited text.
|
|
550
|
+
|
|
551
|
+
**Fallback.** If `AskUserQuestion` is unavailable in the current
|
|
552
|
+
Claude Code version (or the tool call fails), degrade to a
|
|
553
|
+
numbered-list free-text prompt using CONFIRM_HIGH-style parsing:
|
|
554
|
+
|
|
555
|
+
```
|
|
556
|
+
<question-label>:
|
|
557
|
+
|
|
558
|
+
1. <choice 1>
|
|
559
|
+
2. <choice 2>
|
|
560
|
+
...
|
|
561
|
+
N+1. Other (I'll type my own)
|
|
562
|
+
|
|
563
|
+
Reply with the number of your choice, or type your own answer:
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Per-question interaction table
|
|
567
|
+
|
|
568
|
+
| questionId | Mode | Choices (or pre-fill source) |
|
|
569
|
+
| ------------------------------ | ---------------- | ------------------------------------------------------------------------- |
|
|
570
|
+
| `story.audience` | free-text | — |
|
|
571
|
+
| `story.problem` | free-text | — |
|
|
572
|
+
| `story.analogs` | free-text | — |
|
|
573
|
+
| `arch.classification` | selectable | monolith, modular monolith, microservices, serverless, library, CLI, other |
|
|
574
|
+
| `arch.modules` | free-text | — |
|
|
575
|
+
| `arch.entities` | free-text | — |
|
|
576
|
+
| `arch.external_apis` | multi-selectable | `<detected externalApis>` + None + Other |
|
|
577
|
+
| `arch.stack_rationale` | free-text | — |
|
|
578
|
+
| `features.core` | hybrid | pre-fill from readme bullets (projectDescription / headings) |
|
|
579
|
+
| `features.nice_to_have` | hybrid | pre-fill from readme bullets |
|
|
580
|
+
| `features.non_goals` | hybrid | empty pre-fill (always edit from scratch unless user picks `accept`) |
|
|
581
|
+
| `workflow.new_dev_steps` | free-text | — |
|
|
582
|
+
| `workflow.env_values` | free-text | — |
|
|
583
|
+
| `conventions.patterns` | free-text | — |
|
|
584
|
+
| `conventions.errors` | selectable | throw, Result<T,E>, null, silent catch, mixed, other |
|
|
585
|
+
| `conventions.logging` | selectable | console, structured JSON, dedicated logger, none, other |
|
|
586
|
+
| `conventions.api_format` | selectable | REST, GraphQL, gRPC, none, other |
|
|
587
|
+
| `conventions.naming` | free-text | — |
|
|
588
|
+
| `conventions.rules` | free-text | — |
|
|
589
|
+
| `verification.manual` | free-text | — |
|
|
590
|
+
| `verification.staging` | selectable | yes, no |
|
|
591
|
+
| `verification.required_checks` | multi-selectable | `<detected ci.workflows>` + None + Other |
|
|
592
|
+
|
|
593
|
+
10 non-default entries: 5 `selectable`, 2 `multi-selectable`, 3
|
|
594
|
+
`hybrid`. The other 12 questions stay `free-text`.
|
|
595
|
+
|
|
433
596
|
### Rejected-field re-ask routing
|
|
434
597
|
|
|
435
598
|
Fields in `highConfirmedRejected` are re-asked as one sub-question
|
|
@@ -450,6 +613,94 @@ enumeration that `saveSetupState` accepts, matched by prefix. The
|
|
|
450
613
|
`<field>` segment must exactly match a known detector field name AND
|
|
451
614
|
the routing table must map that field to that state prefix.
|
|
452
615
|
|
|
616
|
+
### Detection-skip matrix
|
|
617
|
+
|
|
618
|
+
A question in this table is **auto-skipped** when the listed detection
|
|
619
|
+
field is in `highConfirmedAccepted` (i.e., accepted at CONFIRM_HIGH
|
|
620
|
+
and not rejected). Record the skipped answer in `interviewAnswers` as
|
|
621
|
+
`"[auto-filled from <field>]"` BEFORE evaluating the already-answered
|
|
622
|
+
skip-list. This prevents the interview from re-asking questions the
|
|
623
|
+
scanner already answered.
|
|
624
|
+
|
|
625
|
+
| questionId | Skip when in `highConfirmedAccepted` | Notes |
|
|
626
|
+
| ----------------------- | ---------------------------------------------- | ---------------------------------------- |
|
|
627
|
+
| `story.problem` | `readme` (medium) with non-empty description | README already describes the problem. |
|
|
628
|
+
| `arch.classification` | `monorepo` (high) | Pre-fill `"monorepo"`. |
|
|
629
|
+
| `arch.external_apis` | `externalApis` (high) | Direct mapping. |
|
|
630
|
+
| `workflow.new_dev_steps`| `scripts` (high) AND `readme` (medium) | Both must be present. |
|
|
631
|
+
|
|
632
|
+
All OTHER `questionId`s are always asked — the scanner cannot infer
|
|
633
|
+
them (audience, rationale, conventions, features, etc. are user
|
|
634
|
+
knowledge the detector has no access to).
|
|
635
|
+
|
|
636
|
+
### Field-help table
|
|
637
|
+
|
|
638
|
+
Drives the `?` / `help` command (CONFIRM_HIGH, CONFIRM_MEDIUM, and
|
|
639
|
+
INTERVIEW states) AND the "→ Will be saved as: `<target>`" line on
|
|
640
|
+
every CONFIRM prompt. Keep the `<target>` column stable — parsers
|
|
641
|
+
downstream don't match on it, but users read it for orientation.
|
|
642
|
+
|
|
643
|
+
Detection fields (used at CONFIRM_HIGH and CONFIRM_MEDIUM):
|
|
644
|
+
|
|
645
|
+
| Field | Plain-English description | `<target>` (where accepting lands) | Example answer |
|
|
646
|
+
| ---------------- | ------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------- |
|
|
647
|
+
| `ci` | Which CI provider + workflow files run on push/PR | `## Verification` section of `docs/spec/SPEC.md` | `GitHub Actions, 2 workflows` |
|
|
648
|
+
| `deployment` | Where the app gets deployed | `## Architecture` section of `docs/spec/SPEC.md` | `Vercel` |
|
|
649
|
+
| `envVariables` | Names of env variables the project reads | `## Workflow` section of `docs/spec/SPEC.md` | `DATABASE_URL, STRIPE_SECRET_KEY` |
|
|
650
|
+
| `externalApis` | Third-party APIs / SDKs the project integrates with | `backend-conventions/SKILL.md` External APIs section | `Stripe, Sentry` |
|
|
651
|
+
| `frameworks` | Application frameworks the project uses | `## Tech Stack` section of `CLAUDE.md` | `Next.js 14.2.3, React 18.2.0` |
|
|
652
|
+
| `language` | Primary source language | `## Tech Stack` section of `CLAUDE.md` | `TypeScript` |
|
|
653
|
+
| `linting` | Linters + formatters the project runs | `## Tech Stack` section of `CLAUDE.md` | `ESLint, Prettier` |
|
|
654
|
+
| `monorepo` | Whether the repo is a monorepo + which tool | `## Architecture` section of `docs/spec/SPEC.md` | `pnpm (3 packages)` |
|
|
655
|
+
| `orm` | Database ORM / schema driver | `## Tech Stack` of `CLAUDE.md` + backend-conventions/SKILL | `Prisma` |
|
|
656
|
+
| `packageManager` | How you install dependencies | `## Tech Stack` of `CLAUDE.md` + `## Commands` | `pnpm` |
|
|
657
|
+
| `readme` | Project description pulled from `README.md` | `## Overview` section of `docs/spec/SPEC.md` | a paragraph of text |
|
|
658
|
+
| `scripts` | dev / test / build / lint script names | `## Commands` section of `CLAUDE.md` | `dev=dev test=test build=build lint=lint` |
|
|
659
|
+
| `specDocs` | Spec / design docs already present in the repo | `## Overview` of `docs/spec/SPEC.md` (referenced, not copied) | `2 docs` |
|
|
660
|
+
| `testing` | Test framework + config file | `## Verification` of `docs/spec/SPEC.md` | `vitest (vitest.config.ts)` |
|
|
661
|
+
|
|
662
|
+
Interview questions (used at INTERVIEW states):
|
|
663
|
+
|
|
664
|
+
| `questionId` | Plain-English description | `<target>` | Example answer |
|
|
665
|
+
| ------------------------------ | --------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------- |
|
|
666
|
+
| `story.audience` | Who is this project for? | `## Overview` of `docs/spec/SPEC.md` | `internal developers maintaining our CLI` |
|
|
667
|
+
| `story.problem` | What problem does it solve? | `## Overview` of `docs/spec/SPEC.md` | `scaffolds a worclaude workflow into any repo` |
|
|
668
|
+
| `story.analogs` | Similar products / projects you're modeling after | `## Overview` of `docs/spec/SPEC.md` | `create-react-app, but for Claude workflows` |
|
|
669
|
+
| `arch.classification` | System shape: monolith / services / library / CLI / etc. | `## Architecture` of `docs/spec/SPEC.md` | `modular monolith` |
|
|
670
|
+
| `arch.modules` | Directory purposes and in-house packages | `## Architecture` of `docs/spec/SPEC.md` | `src/commands — CLI entry points` |
|
|
671
|
+
| `arch.entities` | Core database entities (if applicable) | `## Architecture` of `docs/spec/SPEC.md` | `User, Project, Session` |
|
|
672
|
+
| `arch.external_apis` | External APIs beyond SDK detection | `## Architecture` of `docs/spec/SPEC.md` | `Stripe, Sentry` |
|
|
673
|
+
| `arch.stack_rationale` | Why these framework/stack choices | `## Architecture` of `docs/spec/SPEC.md` | `pnpm for workspace perf` |
|
|
674
|
+
| `features.core` | Must-have features | `## Features` of `docs/spec/SPEC.md` | `init / upgrade / doctor / scan` |
|
|
675
|
+
| `features.nice_to_have` | Nice-to-have features | `## Features` of `docs/spec/SPEC.md` | `plugin.json generation` |
|
|
676
|
+
| `features.non_goals` | Explicit non-goals | `## Features` of `docs/spec/SPEC.md` | `no Windows-specific path handling` |
|
|
677
|
+
| `workflow.new_dev_steps` | Dev setup steps beyond README | `## Workflow` of `docs/spec/SPEC.md` | `copy .env.example → .env then fill secrets` |
|
|
678
|
+
| `workflow.env_values` | Guidance for env var values | `## Workflow` of `docs/spec/SPEC.md` | `DATABASE_URL: local Postgres on :5432` |
|
|
679
|
+
| `conventions.patterns` | Code patterns the project uses | `project-patterns/SKILL.md` | `Result<T, E> for fallible ops` |
|
|
680
|
+
| `conventions.errors` | Error-handling approach | `backend-conventions/SKILL.md` | `throw / Result<T,E> / silent catch` |
|
|
681
|
+
| `conventions.logging` | Logging approach | `backend-conventions/SKILL.md` | `pino structured logger` |
|
|
682
|
+
| `conventions.api_format` | API response shape | `backend-conventions/SKILL.md` | `REST JSON: {data, error}` |
|
|
683
|
+
| `conventions.naming` | Naming conventions (vars, files, branches) | `project-patterns/SKILL.md` | `camelCase TS / snake_case Python / kebab branches` |
|
|
684
|
+
| `conventions.rules` | Never / always project rules | `project-patterns/SKILL.md` | `never push to main` |
|
|
685
|
+
| `verification.manual` | Manual verification steps | `## Verification` of `docs/spec/SPEC.md` | `run /init against tmp/ and eyeball output` |
|
|
686
|
+
| `verification.staging` | Whether there's a staging / preview env | `## Verification` of `docs/spec/SPEC.md` | `yes — Vercel preview per PR` |
|
|
687
|
+
| `verification.required_checks` | CI required checks gating merge | `## Verification` of `docs/spec/SPEC.md` | `tests, lint, type-check` |
|
|
688
|
+
|
|
689
|
+
Help-render format when the user types `?` at a CONFIRM or INTERVIEW
|
|
690
|
+
prompt — render in a fenced code block:
|
|
691
|
+
|
|
692
|
+
```
|
|
693
|
+
Help — <formatField(field) or questionId>
|
|
694
|
+
|
|
695
|
+
What it is: <description>
|
|
696
|
+
Will be saved as: <target>
|
|
697
|
+
Example answer: <example>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
For CONFIRM_HIGH (multiple fields on one prompt), render one Help
|
|
701
|
+
block per item. After rendering help, restate the original prompt
|
|
702
|
+
verbatim without advancing.
|
|
703
|
+
|
|
453
704
|
---
|
|
454
705
|
|
|
455
706
|
## Field rendering table
|
|
@@ -470,7 +721,7 @@ CONFIRM_HIGH and CONFIRM_MEDIUM to render `<renderValue(item)>`.
|
|
|
470
721
|
| `scripts` | `{dev, test, build, lint, ...}` | `dev=<dev.key> test=<test.key> build=<build.key> lint=<lint.key>` — omit null slots; all null → `(no standard scripts)` |
|
|
471
722
|
| `envVariables` | `{names: string[], inferredServices: [...]}` | `<N> variable(s)` (singular when `N === 1`; 0 → omit) |
|
|
472
723
|
| `externalApis` | `string[]` | joined by `, ` (empty → omit) |
|
|
473
|
-
| `readme` | `{projectDescription, ...}` | `<projectDescription>`
|
|
724
|
+
| `readme` | `{projectDescription, ...}` | `<projectDescription>` rendered verbatim; soft-wrap at 100 chars per line for display only (no `…` truncation — user needs the full text to decide whether to accept) |
|
|
474
725
|
| `specDocs` | `[{path, firstHeading}, ...]` | `<N> doc(s)` (empty → omit) |
|
|
475
726
|
| `monorepo` | `{tool, packagePaths, ...}` | `<tool> (<N> packages)` |
|
|
476
727
|
| fallback scalar | string / number / boolean | `String(value)` |
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"Bash(git:*)",
|
|
15
15
|
"Bash(gh:*)",
|
|
16
16
|
|
|
17
|
+
"// -- Worclaude Self --",
|
|
18
|
+
"Bash(worclaude:*)",
|
|
19
|
+
"Bash(worclaude scan:*)",
|
|
20
|
+
"Bash(worclaude setup-state:*)",
|
|
21
|
+
|
|
17
22
|
"// -- Common Dev Tools --",
|
|
18
23
|
"Bash(echo:*)", "Bash(mkdir:*)", "Bash(touch:*)",
|
|
19
24
|
"Bash(cp:*)", "Bash(mv:*)",
|