joycraft 0.6.15 → 0.6.16

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/README.md CHANGED
@@ -31,7 +31,15 @@ Most developers plateau at Level 2. Joycraft's job is to move you up.
31
31
 
32
32
  ### Platform support
33
33
 
34
- Joycraft supports **Claude Code**, **OpenAI Codex**, and **Pi** out of the box. Running `npx joycraft init` auto-detects which harnesses your project uses and installs the matching skills no flags, no configuration:
34
+ Joycraft supports **Claude Code**, **OpenAI Codex**, and **Pi** out of the box. When you run `npx joycraft init`, it opens with a quick picker choose any combination of the three, and only the harnesses you select get installed:
35
+
36
+ ```
37
+ Which AI harnesses should Joycraft install?
38
+ claude — Claude Code (.claude/)
39
+ codex — OpenAI Codex (.agents/)
40
+ pi — Pi (.pi/)
41
+ Harnesses [comma-separated, or "all"] (none): claude,pi
42
+ ```
35
43
 
36
44
  | Harness | Skills installed to | Invocation |
37
45
  |---------|---------------------|------------|
@@ -39,7 +47,7 @@ Joycraft supports **Claude Code**, **OpenAI Codex**, and **Pi** out of the box.
39
47
  | Codex | `.agents/skills/` (+ `AGENTS.md`) | `$joycraft-*` |
40
48
  | Pi | `.pi/skills/` (+ pipeline runtime, see below) | `/skill:joycraft-*` |
41
49
 
42
- All three get the same structured workflows, adapted for each tool's invocation model. Codex and Pi surfaces install when their directory (`.agents/` or `.pi/`) is present in the project.
50
+ All three get the same structured workflows, adapted for each tool's invocation model. A single-harness install carries **no footprint from the others** — pick `codex` only and you get `.agents/` with no `.claude/` or `.pi/` in sight. In a non-interactive run (CI, piped, no TTY) `init` installs all three so existing scripts keep working. The shared docs (`CLAUDE.md`, `AGENTS.md`, `docs/`) are written regardless of which harnesses you pick.
43
51
 
44
52
  ### Headless spec execution (Pi)
45
53
 
@@ -70,16 +78,19 @@ cd /path/to/your/project
70
78
  npx joycraft init
71
79
  ```
72
80
 
73
- Joycraft auto-detects your tech stack and creates:
81
+ `init` first asks which harnesses to install (see [Platform support](#platform-support) above), then auto-detects your tech stack and creates:
74
82
 
75
83
  - **CLAUDE.md** with behavioral boundaries (Always / Ask First / Never) and correct build/test/lint commands
76
- - **AGENTS.md** for Codex compatibility
77
- - **20 skills** installed to `.claude/skills/` (Claude Code), `.agents/skills/` (Codex), and `.pi/skills/` (Pi) — see [Which skill do I need?](#which-skill-do-i-need) below
78
- - **Pi pipeline runtime** in `.pi/scripts/joycraft/` (when `.pi/` is present) — the headless spec-execution driver and its helpers
79
- - **docs/** structure: `docs/context/` is created up front; feature work lands in `docs/features/<slug>/{brief.md, research.md, design.md, specs/}` and deferred work in `docs/backlog/` these are created lazily by the skills that write to them
84
+ - **AGENTS.md** for Codex/Pi compatibility
85
+ - **20 skills** installed to the selected harnesses — `.claude/skills/` (Claude Code), `.agents/skills/` (Codex), and/or `.pi/skills/` (Pi) — see [Which skill do I need?](#which-skill-do-i-need) below
86
+ - **Pi pipeline runtime** in `.pi/scripts/joycraft/` (when Pi is selected) — the headless spec-execution driver and its helpers
87
+ - **Agent teams enabled** when Claude Code is selected, `init` sets `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in `.claude/settings.json` so subagent-driven skills like `/joycraft-research` work out of the box (idempotent it never clobbers a value you already set)
88
+ - **docs/** structure: `docs/context/` is created up front; feature work lands in `docs/features/<slug>/{brief.md, research.md, design.md, specs/}` and deferred work in `docs/backlog/` — these are created lazily by the skills that write to them. Joycraft's own upgrade state lives hidden at `docs/.joycraft/state.json` (harness-neutral, gitignored — never committed)
80
89
  - **Context documents** in `docs/context/`: production map, dangerous assumptions, decision log, institutional knowledge, and troubleshooting guide
81
90
  - **Templates** including atomic spec, feature brief, implementation plan, boundary framework, and workflow templates for scenario generation and autofix loops
82
91
 
92
+ > Pick nothing at the harness prompt and `init` installs nothing — it tells you to re-run and choose at least one harness.
93
+
83
94
  ### Git tracking: shared vs private
84
95
 
85
96
  By default Joycraft assumes you want to **commit** the harness so your whole team
@@ -91,21 +102,26 @@ npx joycraft init --gitignore=shared # default — commit .claude/, .agents/,
91
102
  npx joycraft init --gitignore=private # gitignore them; track only CLAUDE.md, AGENTS.md, docs/
92
103
  ```
93
104
 
94
- Run interactively without the flag and `init` will ask. The choice is saved, so
95
- `npx joycraft upgrade` re-applies it automatically. To switch an existing
96
- project later (or decide from CI), pass the same flag to upgrade:
97
- `npx joycraft upgrade --gitignore=private`. `.gitignore` edits are
98
- append-only — Joycraft never rewrites or removes your existing lines.
105
+ Run interactively without the flag and `init` asks (right after the harness
106
+ picker). The choice is saved, so `npx joycraft upgrade` re-applies it
107
+ automatically. To switch an existing project later (or decide from CI), pass the
108
+ same flag to upgrade: `npx joycraft upgrade --gitignore=private`. `.gitignore`
109
+ edits are append-only — Joycraft never rewrites or removes your existing lines.
99
110
 
100
111
  | Profile | Tracked in git | Gitignored |
101
112
  |---------|----------------|------------|
102
- | `shared` (default) | `CLAUDE.md`, `AGENTS.md`, `docs/`, `.claude/skills/`, `.agents/`, `.pi/` | hidden upgrade state only |
113
+ | `shared` (default) | `CLAUDE.md`, `AGENTS.md`, `docs/`, `.claude/skills/`, `.agents/`, `.pi/` | hidden upgrade state only (`docs/.joycraft/state.json`) |
103
114
  | `private` | `CLAUDE.md`, `AGENTS.md`, `docs/` | `.claude/`, `.agents/`, `.pi/` |
104
115
 
105
116
  > Switching an existing project to `private` only updates `.gitignore`. If
106
117
  > harness files were already committed, untrack them with
107
118
  > `git rm -r --cached .claude .agents .pi` (Joycraft prints this reminder and
108
119
  > never runs git for you).
120
+ >
121
+ > Under `private`, the harness dirs aren't committed — so a teammate who clones
122
+ > the repo gets `CLAUDE.md`/`AGENTS.md` but no skills until they run
123
+ > `npx joycraft init` to regenerate them locally. Joycraft adds a one-line
124
+ > reminder to your generated `CLAUDE.md` and `AGENTS.md` for exactly this reason.
109
125
 
110
126
  ### Supported Stacks
111
127
 
@@ -198,6 +214,8 @@ npx joycraft upgrade
198
214
 
199
215
  Joycraft tracks what it installed vs. what you've customized. Unmodified files update automatically. Customized files show a diff and ask before overwriting. Use `--yes` for CI.
200
216
 
217
+ `upgrade` only refreshes the harnesses you installed at init — a Codex-only project stays Codex-only and never grows a `.claude/` tree. (Projects from before harness selection existed have no recorded choice, so `upgrade` refreshes all three, preserving the old behavior.)
218
+
201
219
  > **Note:** If you're upgrading from an early version, deprecated skill directories (e.g., `/joy`, `/joysmith`, `/tune`) are automatically removed during upgrade.
202
220
 
203
221
  ## Why This Exists
@@ -24,7 +24,7 @@ function ensureGitignoreEntries(targetDir, entries) {
24
24
  }
25
25
  function applyGitignoreProfile(targetDir, profile) {
26
26
  if (profile === "private") {
27
- return ensureGitignoreEntries(targetDir, PRIVATE_PROFILE_IGNORES);
27
+ return ensureGitignoreEntries(targetDir, [...PRIVATE_PROFILE_IGNORES, STATE_PATH]);
28
28
  }
29
29
  return ensureGitignoreEntries(targetDir, [STATE_PATH]);
30
30
  }
@@ -83,4 +83,4 @@ export {
83
83
  validateGitignoreFlag,
84
84
  resolveGitignoreProfile
85
85
  };
86
- //# sourceMappingURL=chunk-YE4LWG2O.js.map
86
+ //# sourceMappingURL=chunk-G6WSFZQG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gitignore.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport {\n STATE_PATH,\n parseGitignoreProfile,\n DEFAULT_GITIGNORE_PROFILE,\n type GitignoreProfile,\n} from './version.js';\n\n/**\n * The harness directories the `private` profile gitignores. Tracking only\n * CLAUDE.md, AGENTS.md, and docs/ means everything under these three dirs\n * stays local. `.claude/` already covers the hidden state file.\n *\n * Single source of truth: every user-facing string that names these dirs\n * (prompts, summaries, the untrack hint, CLI help) derives from this list via\n * the constants below, so adding a harness dir can't leave stale messages.\n */\nexport const PRIVATE_PROFILE_IGNORES = ['.claude/', '.agents/', '.pi/'];\n\n/** Human-readable list of the private-profile dirs, for prompts and summaries. */\nexport const PRIVATE_DIRS_DISPLAY = PRIVATE_PROFILE_IGNORES.join(', ');\n\n/** Copy-pasteable command to untrack already-committed harness files. */\nexport const PRIVATE_UNTRACK_COMMAND = `git rm -r --cached ${PRIVATE_PROFILE_IGNORES.map((d) => d.replace(/\\/$/, '')).join(' ')}`;\n\n/**\n * Append-only, create-if-absent, idempotent .gitignore writer.\n *\n * Mirrors the \"append over modify when touching user files\" principle: it never\n * rewrites, reorders, or removes existing lines — it only appends the entries\n * not already present (matched exactly, after trimming). One read + at most one\n * write per call. Returns the entries actually added (empty when everything was\n * already present).\n */\nexport function ensureGitignoreEntries(targetDir: string, entries: string[]): string[] {\n const gitignorePath = join(targetDir, '.gitignore');\n const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';\n const present = new Set(current.split('\\n').map((l) => l.trim()));\n const missing = entries.filter((e) => !present.has(e.trim()));\n if (missing.length === 0) return [];\n\n // Append on their own lines, tolerating a file that may or may not end in \\n.\n const sep = current.length > 0 && !current.endsWith('\\n') ? '\\n' : '';\n writeFileSync(gitignorePath, current + sep + missing.join('\\n') + '\\n', 'utf-8');\n return missing;\n}\n\n/** Single-entry convenience wrapper around ensureGitignoreEntries. */\nexport function ensureGitignoreEntry(targetDir: string, line: string): boolean {\n return ensureGitignoreEntries(targetDir, [line]).length > 0;\n}\n\n/**\n * Apply a gitignore profile's entries to the project's .gitignore.\n *\n * The hidden upgrade-state file (`STATE_PATH`) is tool-managed, regenerated on\n * every init/upgrade, and must never be committed — under BOTH profiles. It now\n * lives at `docs/.joycraft/state.json`; since `docs/` is always tracked, the\n * state entry is no longer covered transitively by any harness-dir ignore, so\n * both profiles list it explicitly.\n *\n * - `shared` — ignore only the hidden state file (commit the harness dirs).\n * - `private` — ignore the .claude/, .agents/, .pi/ trees AND the state file.\n *\n * Append-only and idempotent (via ensureGitignoreEntries), so re-running\n * init/upgrade never duplicates entries. Returns the list of lines actually\n * added this call.\n */\nexport function applyGitignoreProfile(targetDir: string, profile: GitignoreProfile): string[] {\n if (profile === 'private') {\n return ensureGitignoreEntries(targetDir, [...PRIVATE_PROFILE_IGNORES, STATE_PATH]);\n }\n // `shared`: only the hidden state file, matching long-standing behavior.\n return ensureGitignoreEntries(targetDir, [STATE_PATH]);\n}\n\n/** A resolved profile plus how it was arrived at. */\nexport interface ResolvedGitignoreProfile {\n profile: GitignoreProfile;\n /**\n * True when the profile is an actual decision: a --gitignore flag, a\n * persisted choice, or an interactive answer. False when it is the\n * non-interactive fallback default — callers must NOT persist a fallback,\n * or the one-time prompt would be permanently suppressed for the project.\n */\n decided: boolean;\n}\n\n/** Validate a raw --gitignore flag value. Throws the user-facing error on unknown values. */\nexport function validateGitignoreFlag(flag: string): GitignoreProfile {\n const parsed = parseGitignoreProfile(flag);\n if (!parsed) {\n throw new Error(`Unknown gitignore profile '${flag}'. Use 'shared' or 'private'.`);\n }\n return parsed;\n}\n\n/**\n * Prompt for a gitignore profile. An empty answer takes the default; an\n * unrecognized answer re-asks instead of being silently coerced — a typo must\n * not get persisted as a permanent choice.\n */\nasync function promptGitignoreProfile(intro: string): Promise<GitignoreProfile> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n console.log(intro);\n console.log(' shared — commit skills so your team gets the same workflow (default)');\n console.log(` private — gitignore ${PRIVATE_DIRS_DISPLAY}; track only CLAUDE.md, AGENTS.md, docs/`);\n return new Promise((resolve) => {\n const ask = (): void => {\n rl.question(`Profile [shared/private] (${DEFAULT_GITIGNORE_PROFILE}): `, (answer) => {\n if (answer.trim() === '') {\n rl.close();\n resolve(DEFAULT_GITIGNORE_PROFILE);\n return;\n }\n const parsed = parseGitignoreProfile(answer);\n if (parsed) {\n rl.close();\n resolve(parsed);\n return;\n }\n console.log(\n `Unrecognized answer '${answer.trim()}' — type 'shared' or 'private', or press Enter for ${DEFAULT_GITIGNORE_PROFILE}.`\n );\n ask();\n });\n };\n ask();\n });\n}\n\n/**\n * Resolve the gitignore profile by precedence — the single resolver shared by\n * init and upgrade so the two commands can never drift:\n * 1. --gitignore flag (validated; throws on unknown value)\n * 2. profile persisted in state.json (re-init / upgrade keep the prior choice)\n * 3. interactive prompt (the caller decides when prompting is allowed)\n * 4. fallback default `shared`, reported with decided=false\n */\nexport async function resolveGitignoreProfile(opts: {\n /** Raw --gitignore value from the CLI, if provided. */\n flag?: string;\n /** Profile previously persisted in state.json, if any. */\n persisted?: GitignoreProfile;\n /** Whether prompting is allowed (TTY, and the command permits interaction). */\n interactive: boolean;\n /** First line printed above the prompt; init and upgrade word it differently. */\n promptIntro: string;\n}): Promise<ResolvedGitignoreProfile> {\n if (opts.flag !== undefined) {\n return { profile: validateGitignoreFlag(opts.flag), decided: true };\n }\n if (opts.persisted) {\n return { profile: opts.persisted, decided: true };\n }\n if (opts.interactive) {\n return { profile: await promptGitignoreProfile(opts.promptIntro), decided: true };\n }\n return { profile: DEFAULT_GITIGNORE_PROFILE, decided: false };\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,YAAY,cAAc,qBAAqB;AACxD,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAiBzB,IAAM,0BAA0B,CAAC,YAAY,YAAY,MAAM;AAG/D,IAAM,uBAAuB,wBAAwB,KAAK,IAAI;AAG9D,IAAM,0BAA0B,sBAAsB,wBAAwB,IAAI,CAAC,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC;AAWxH,SAAS,uBAAuB,WAAmB,SAA6B;AACrF,QAAM,gBAAgB,KAAK,WAAW,YAAY;AAClD,QAAM,UAAU,WAAW,aAAa,IAAI,aAAa,eAAe,OAAO,IAAI;AACnF,QAAM,UAAU,IAAI,IAAI,QAAQ,MAAM,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAChE,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,KAAK,CAAC,CAAC;AAC5D,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAGlC,QAAM,MAAM,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,IAAI,IAAI,OAAO;AACnE,gBAAc,eAAe,UAAU,MAAM,QAAQ,KAAK,IAAI,IAAI,MAAM,OAAO;AAC/E,SAAO;AACT;AAuBO,SAAS,sBAAsB,WAAmB,SAAqC;AAC5F,MAAI,YAAY,WAAW;AACzB,WAAO,uBAAuB,WAAW,CAAC,GAAG,yBAAyB,UAAU,CAAC;AAAA,EACnF;AAEA,SAAO,uBAAuB,WAAW,CAAC,UAAU,CAAC;AACvD;AAeO,SAAS,sBAAsB,MAAgC;AACpE,QAAM,SAAS,sBAAsB,IAAI;AACzC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,8BAA8B,IAAI,+BAA+B;AAAA,EACnF;AACA,SAAO;AACT;AAOA,eAAe,uBAAuB,OAA0C;AAC9E,QAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,UAAQ,IAAI,KAAK;AACjB,UAAQ,IAAI,8EAAyE;AACrF,UAAQ,IAAI,8BAAyB,oBAAoB,0CAA0C;AACnG,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,MAAM,MAAY;AACtB,SAAG,SAAS,6BAA6B,yBAAyB,OAAO,CAAC,WAAW;AACnF,YAAI,OAAO,KAAK,MAAM,IAAI;AACxB,aAAG,MAAM;AACT,kBAAQ,yBAAyB;AACjC;AAAA,QACF;AACA,cAAM,SAAS,sBAAsB,MAAM;AAC3C,YAAI,QAAQ;AACV,aAAG,MAAM;AACT,kBAAQ,MAAM;AACd;AAAA,QACF;AACA,gBAAQ;AAAA,UACN,wBAAwB,OAAO,KAAK,CAAC,2DAAsD,yBAAyB;AAAA,QACtH;AACA,YAAI;AAAA,MACN,CAAC;AAAA,IACH;AACA,QAAI;AAAA,EACN,CAAC;AACH;AAUA,eAAsB,wBAAwB,MASR;AACpC,MAAI,KAAK,SAAS,QAAW;AAC3B,WAAO,EAAE,SAAS,sBAAsB,KAAK,IAAI,GAAG,SAAS,KAAK;AAAA,EACpE;AACA,MAAI,KAAK,WAAW;AAClB,WAAO,EAAE,SAAS,KAAK,WAAW,SAAS,KAAK;AAAA,EAClD;AACA,MAAI,KAAK,aAAa;AACpB,WAAO,EAAE,SAAS,MAAM,uBAAuB,KAAK,WAAW,GAAG,SAAS,KAAK;AAAA,EAClF;AACA,SAAO,EAAE,SAAS,2BAA2B,SAAS,MAAM;AAC9D;","names":[]}