joycraft 0.6.15 → 0.6.17

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.
@@ -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
  }
@@ -77,10 +77,11 @@ async function resolveGitignoreProfile(opts) {
77
77
  }
78
78
 
79
79
  export {
80
+ PRIVATE_PROFILE_IGNORES,
80
81
  PRIVATE_DIRS_DISPLAY,
81
82
  PRIVATE_UNTRACK_COMMAND,
82
83
  applyGitignoreProfile,
83
84
  validateGitignoreFlag,
84
85
  resolveGitignoreProfile
85
86
  };
86
- //# sourceMappingURL=chunk-YE4LWG2O.js.map
87
+ //# sourceMappingURL=chunk-GWDLD7SH.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":[]}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PRIVATE_DIRS_DISPLAY
4
- } from "./chunk-YE4LWG2O.js";
4
+ } from "./chunk-GWDLD7SH.js";
5
5
  import "./chunk-34IWIKXS.js";
6
6
 
7
7
  // src/cli.ts
@@ -15,7 +15,7 @@ var GITIGNORE_OPTION_DESC = `Gitignore profile: 'shared' (commit skills) or 'pri
15
15
  var program = new Command();
16
16
  program.name("joycraft").description("Scaffold and upgrade AI development harnesses").version(pkg.version, "-v, --version");
17
17
  program.command("init").description("Scaffold the Joycraft harness into the current project").argument("[dir]", "Target directory", ".").option("--force", "Overwrite existing files").option("--gitignore <profile>", GITIGNORE_OPTION_DESC).action(async (dir, opts) => {
18
- const { init } = await import("./init-LXSMLAY5.js");
18
+ const { init } = await import("./init-OO77DURX.js");
19
19
  try {
20
20
  await init(dir, { force: opts.force ?? false, gitignore: opts.gitignore });
21
21
  } catch (err) {
@@ -24,7 +24,7 @@ program.command("init").description("Scaffold the Joycraft harness into the curr
24
24
  }
25
25
  });
26
26
  program.command("upgrade").description("Upgrade installed Joycraft templates and skills to latest").argument("[dir]", "Target directory", ".").option("--yes", "Auto-accept all updates").option("--gitignore <profile>", GITIGNORE_OPTION_DESC).action(async (dir, opts) => {
27
- const { upgrade } = await import("./upgrade-P3JZS7NM.js");
27
+ const { upgrade } = await import("./upgrade-VKQ54KRD.js");
28
28
  try {
29
29
  await upgrade(dir, { yes: opts.yes ?? false, gitignore: opts.gitignore });
30
30
  } catch (err) {
@@ -33,7 +33,7 @@ program.command("upgrade").description("Upgrade installed Joycraft templates and
33
33
  }
34
34
  });
35
35
  program.command("init-autofix").description("Set up the Level 5 auto-fix loop with holdout scenarios").argument("[dir]", "Target directory", ".").option("--scenarios-repo <name>", "Name for scenarios repo").option("--app-id <id>", "GitHub App ID for Joycraft Autofix").option("--force", "Overwrite existing workflow files").option("--dry-run", "Show what would be created without creating it").action(async (dir, opts) => {
36
- const { initAutofix } = await import("./init-autofix-OZW5ITFI.js");
36
+ const { initAutofix } = await import("./init-autofix-6GMXHF5F.js");
37
37
  await initAutofix(dir, opts);
38
38
  });
39
39
  program.command("check-version").description("Check if a newer version of Joycraft is available").action(async () => {
@@ -7,7 +7,7 @@ import {
7
7
  PRIVATE_UNTRACK_COMMAND,
8
8
  applyGitignoreProfile,
9
9
  resolveGitignoreProfile
10
- } from "./chunk-YE4LWG2O.js";
10
+ } from "./chunk-GWDLD7SH.js";
11
11
  import {
12
12
  CODEX_SKILLS,
13
13
  PI_AGENTS,
@@ -16,7 +16,7 @@ import {
16
16
  PI_SKILLS,
17
17
  SKILLS,
18
18
  TEMPLATES
19
- } from "./chunk-XOMQIK4U.js";
19
+ } from "./chunk-3JVMOJTA.js";
20
20
  import {
21
21
  DEFAULT_GITIGNORE_PROFILE,
22
22
  STATE_PATH,
@@ -27,8 +27,8 @@ import {
27
27
  } from "./chunk-34IWIKXS.js";
28
28
 
29
29
  // src/init.ts
30
- import { mkdirSync as mkdirSync2, existsSync as existsSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync, chmodSync } from "fs";
31
- import { join as join4, basename, resolve, dirname } from "path";
30
+ import { mkdirSync as mkdirSync2, existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync, chmodSync } from "fs";
31
+ import { join as join5, basename, resolve, dirname } from "path";
32
32
 
33
33
  // src/detect.ts
34
34
  import { readFileSync, existsSync, readdirSync } from "fs";
@@ -313,7 +313,7 @@ import { existsSync as existsSync2 } from "fs";
313
313
  import { join as join2 } from "path";
314
314
  var PRIVATE_SETUP_NOTE_MARKER = "After cloning, run";
315
315
  function generatePrivateSetupNote() {
316
- return `> **Private setup:** The harness dirs (\`.claude/\`, \`.agents/\`, \`.pi/\`) are gitignored in this repo, so they aren't committed. ${PRIVATE_SETUP_NOTE_MARKER} \`npx joycraft init\` to regenerate the skill files locally.`;
316
+ return `> **Private setup:** The harness dirs (\`.claude/\`, \`.agents/\`, \`.pi/\`) are gitignored in this repo, so they aren't committed. ${PRIVATE_SETUP_NOTE_MARKER} \`npx joycraft init\` to regenerate the skill files locally \u2014 it only creates missing files and leaves your committed \`CLAUDE.md\`, \`AGENTS.md\`, and \`docs/\` untouched (use \`--force\` only if you deliberately want to regenerate them).`;
317
317
  }
318
318
  function generateCommandsBlock(stack) {
319
319
  const lines = ["```bash"];
@@ -365,7 +365,9 @@ ${generateCommandsBlock(stack)}
365
365
 
366
366
  **Default execution mode:** batch
367
367
 
368
- _How \`/joycraft-implement\` wraps up after each spec. \`joycraft-decompose\` reads this line (absent \u21D2 \`batch\`) and recommends a per-spec mode you approve. Modes: \`batch\` (implement a cluster, wrap once at the end), \`checkpoint\` (commit + status bump after each spec), \`isolated\` (fresh context per spec \u2014 on Pi, the \`joycraft-implement-loop\` driver). Change the value above to set your project default._`;
368
+ _How \`/joycraft-implement\` wraps up after each spec. \`joycraft-decompose\` reads this line (absent \u21D2 \`batch\`) and recommends a per-spec mode you approve. Modes: \`batch\` (implement a cluster, wrap once at the end), \`checkpoint\` (commit + status bump after each spec), \`isolated\` (fresh context per spec \u2014 on Pi, the \`joycraft-implement-loop\` driver). Change the value above to set your project default._
369
+
370
+ **Deferred work \u2192 \`docs/backlog/\`.** Ideas and follow-ups you surface mid-sprint but can't take on now go to \`docs/backlog/\` (one file per item) so the current spec stays focused without losing the thread. Promote an entry to a Feature Brief under \`docs/features/<slug>/\` when you're ready to build it.`;
369
371
  }
370
372
  function generateArchitectureSection() {
371
373
  return `## Architecture
@@ -487,7 +489,9 @@ function generateExternalApiSafetySection() {
487
489
  function generateDevelopmentSection(stack) {
488
490
  return `## Development
489
491
 
490
- ${generateCommandsBlock2(stack)}`;
492
+ ${generateCommandsBlock2(stack)}
493
+
494
+ **Deferred work \u2192 \`docs/backlog/\`.** Ideas and follow-ups surfaced mid-sprint that can't be taken on now go to \`docs/backlog/\` (one file per item) so the current spec stays focused. Promote an entry to a Feature Brief under \`docs/features/<slug>/\` when ready to build it.`;
491
495
  }
492
496
  function generateArchitectureSection2() {
493
497
  return `## Architecture
@@ -713,25 +717,113 @@ function installSafeguardHooks(targetDir, customPatterns = [], force = false, sk
713
717
  return result;
714
718
  }
715
719
 
720
+ // src/tsconfig.ts
721
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
722
+ import { join as join4 } from "path";
723
+ var PI_EXCLUDE = ".pi";
724
+ function stripJsonComments(text) {
725
+ return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
726
+ }
727
+ function alreadyExcludesPi(rawText) {
728
+ try {
729
+ const parsed = JSON.parse(stripJsonComments(rawText));
730
+ if (!Array.isArray(parsed.exclude)) return false;
731
+ return parsed.exclude.some(
732
+ (e) => typeof e === "string" && (e === PI_EXCLUDE || e === "./.pi" || e === ".pi/**")
733
+ );
734
+ } catch {
735
+ return false;
736
+ }
737
+ }
738
+ function insertPiExclude(rawText) {
739
+ const excludeArrayRe = /("exclude"\s*:\s*\[)(\s*)/;
740
+ if (excludeArrayRe.test(rawText)) {
741
+ return rawText.replace(excludeArrayRe, (_m, open, ws) => {
742
+ const sep = ws.includes("\n") ? ws : " ";
743
+ return `${open}${sep}"${PI_EXCLUDE}",${ws.includes("\n") ? "" : " "}`;
744
+ });
745
+ }
746
+ const includeArrayRe = /("include"\s*:\s*\[[^\]]*\])/;
747
+ if (includeArrayRe.test(rawText)) {
748
+ return rawText.replace(includeArrayRe, (m) => `${m},
749
+ "exclude": ["${PI_EXCLUDE}"]`);
750
+ }
751
+ const lastBrace = rawText.lastIndexOf("}");
752
+ if (lastBrace > 0 && rawText.slice(0, lastBrace).includes(":")) {
753
+ const before = rawText.slice(0, lastBrace).replace(/\s*$/, "");
754
+ const after = rawText.slice(lastBrace);
755
+ const needsComma = !before.endsWith(",") && !before.endsWith("{");
756
+ return `${before}${needsComma ? "," : ""}
757
+ "exclude": ["${PI_EXCLUDE}"]
758
+ ${after}`;
759
+ }
760
+ return null;
761
+ }
762
+ function ensurePiExcludedFromTsconfig(targetDir) {
763
+ const path = join4(targetDir, "tsconfig.json");
764
+ if (!existsSync4(path)) return { status: "no-tsconfig" };
765
+ let rawText;
766
+ try {
767
+ rawText = readFileSync3(path, "utf-8");
768
+ } catch {
769
+ return { status: "skipped", reason: "tsconfig.json could not be read" };
770
+ }
771
+ if (alreadyExcludesPi(rawText)) {
772
+ return { status: "already-present", path };
773
+ }
774
+ const updated = insertPiExclude(rawText);
775
+ if (updated === null || updated === rawText) {
776
+ return {
777
+ status: "skipped",
778
+ reason: 'could not safely edit tsconfig.json \u2014 add ".pi" to its "exclude" array manually so the Pi extension stays out of your TypeScript build'
779
+ };
780
+ }
781
+ if (!alreadyExcludesPi(updated)) {
782
+ return {
783
+ status: "skipped",
784
+ reason: 'tsconfig.json edit could not be verified \u2014 add ".pi" to its "exclude" array manually'
785
+ };
786
+ }
787
+ try {
788
+ writeFileSync2(path, updated, "utf-8");
789
+ } catch {
790
+ return { status: "skipped", reason: "tsconfig.json could not be written" };
791
+ }
792
+ return { status: "added", path };
793
+ }
794
+
716
795
  // src/init.ts
796
+ var BACKLOG_README = `# Backlog
797
+
798
+ Deferred work lives here \u2014 ideas and follow-ups you surface mid-sprint but
799
+ can't take on in the current feature. Capturing them keeps the current spec
800
+ focused without losing the thread.
801
+
802
+ - One file per item: \`docs/backlog/YYYY-MM-DD-<short-name>.md\`.
803
+ - Joycraft skills (\`/joycraft-interview\`, \`/joycraft-new-feature\`,
804
+ \`/joycraft-design\`) offer to write entries here \u2014 always with your
805
+ confirmation, never automatically.
806
+ - Promote an item by turning it into a Feature Brief under
807
+ \`docs/features/<slug>/\` when you're ready to build it.
808
+ `;
717
809
  function ensureDir(dir) {
718
- if (!existsSync4(dir)) {
810
+ if (!existsSync5(dir)) {
719
811
  mkdirSync2(dir, { recursive: true });
720
812
  }
721
813
  }
722
814
  function writeFile(path, content, force, result) {
723
- if (existsSync4(path) && !force) {
815
+ if (existsSync5(path) && !force) {
724
816
  result.skipped.push(path);
725
817
  return;
726
818
  }
727
- writeFileSync2(path, content, "utf-8");
819
+ writeFileSync3(path, content, "utf-8");
728
820
  result.created.push(path);
729
821
  }
730
822
  async function init(dir, opts) {
731
823
  const targetDir = resolve(dir);
732
824
  const result = { created: [], skipped: [], modified: [], warnings: [] };
733
825
  const stack = await detectStack(targetDir);
734
- const isPi = existsSync4(join4(targetDir, ".pi"));
826
+ const isPi = existsSync5(join5(targetDir, ".pi"));
735
827
  const harnesses = await resolveHarnesses(process.stdin.isTTY === true);
736
828
  const wants = (h) => harnesses.includes(h);
737
829
  if (harnesses.length === 0) {
@@ -746,14 +838,16 @@ async function init(dir, opts) {
746
838
  interactive: process.stdin.isTTY === true,
747
839
  promptIntro: "\nHow should Joycraft files be tracked in git?"
748
840
  });
749
- ensureDir(join4(targetDir, "docs", "context"));
750
- const skillsDir = join4(targetDir, ".claude", "skills");
841
+ ensureDir(join5(targetDir, "docs", "context"));
842
+ ensureDir(join5(targetDir, "docs", "backlog"));
843
+ writeFile(join5(targetDir, "docs", "backlog", "README.md"), BACKLOG_README, opts.force, result);
844
+ const skillsDir = join5(targetDir, ".claude", "skills");
751
845
  let existingSkills = [];
752
- if (wants("claude") && existsSync4(skillsDir)) {
846
+ if (wants("claude") && existsSync5(skillsDir)) {
753
847
  existingSkills = readdirSync2(skillsDir).filter((name) => {
754
848
  if (name.startsWith("joycraft-")) return false;
755
849
  if (name.startsWith(".")) return false;
756
- const fullPath = join4(skillsDir, name);
850
+ const fullPath = join5(skillsDir, name);
757
851
  try {
758
852
  return statSync(fullPath).isDirectory();
759
853
  } catch {
@@ -764,32 +858,32 @@ async function init(dir, opts) {
764
858
  if (wants("claude")) {
765
859
  for (const [filename, content] of Object.entries(SKILLS)) {
766
860
  const skillName = filename.replace(/\.md$/, "");
767
- const skillDir = join4(skillsDir, skillName);
861
+ const skillDir = join5(skillsDir, skillName);
768
862
  ensureDir(skillDir);
769
- writeFile(join4(skillDir, "SKILL.md"), content, opts.force, result);
863
+ writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
770
864
  }
771
865
  }
772
866
  if (wants("codex")) {
773
- const codexSkillsDir = join4(targetDir, ".agents", "skills");
867
+ const codexSkillsDir = join5(targetDir, ".agents", "skills");
774
868
  for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
775
869
  const skillName = filename.replace(/\.md$/, "");
776
- const skillDir = join4(codexSkillsDir, skillName);
870
+ const skillDir = join5(codexSkillsDir, skillName);
777
871
  ensureDir(skillDir);
778
- writeFile(join4(skillDir, "SKILL.md"), content, opts.force, result);
872
+ writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
779
873
  }
780
874
  }
781
875
  if (wants("pi")) {
782
- const piSkillsDir = join4(targetDir, ".pi", "skills");
876
+ const piSkillsDir = join5(targetDir, ".pi", "skills");
783
877
  for (const [filename, content] of Object.entries(PI_SKILLS)) {
784
878
  const skillName = filename.replace(/\.md$/, "");
785
- const skillDir = join4(piSkillsDir, skillName);
879
+ const skillDir = join5(piSkillsDir, skillName);
786
880
  ensureDir(skillDir);
787
- writeFile(join4(skillDir, "SKILL.md"), content, opts.force, result);
881
+ writeFile(join5(skillDir, "SKILL.md"), content, opts.force, result);
788
882
  }
789
- const piScriptsDir = join4(targetDir, ".pi", "scripts", "joycraft");
883
+ const piScriptsDir = join5(targetDir, ".pi", "scripts", "joycraft");
790
884
  ensureDir(piScriptsDir);
791
885
  for (const [name, content] of Object.entries(PI_SCRIPTS)) {
792
- const scriptPath = join4(piScriptsDir, name);
886
+ const scriptPath = join5(piScriptsDir, name);
793
887
  writeFile(scriptPath, content, opts.force, result);
794
888
  if (name !== "README.md") {
795
889
  try {
@@ -798,78 +892,89 @@ async function init(dir, opts) {
798
892
  }
799
893
  }
800
894
  }
801
- const piExtDir = join4(targetDir, ".pi", "extensions");
895
+ const piExtDir = join5(targetDir, ".pi", "extensions");
802
896
  ensureDir(piExtDir);
803
897
  for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
804
- writeFile(join4(piExtDir, name), content, opts.force, result);
898
+ writeFile(join5(piExtDir, name), content, opts.force, result);
805
899
  }
806
- const piAgentsDir = join4(targetDir, ".pi", "agents");
900
+ const piAgentsDir = join5(targetDir, ".pi", "agents");
807
901
  ensureDir(piAgentsDir);
808
902
  for (const [name, content] of Object.entries(PI_AGENTS)) {
809
- writeFile(join4(piAgentsDir, name), content, opts.force, result);
903
+ writeFile(join5(piAgentsDir, name), content, opts.force, result);
810
904
  }
811
905
  }
812
- const templatesDir = join4(targetDir, "docs", "templates");
906
+ const templatesDir = join5(targetDir, "docs", "templates");
813
907
  ensureDir(templatesDir);
814
908
  for (const [filename, content] of Object.entries(TEMPLATES)) {
815
- ensureDir(dirname(join4(templatesDir, filename)));
816
- writeFile(join4(templatesDir, filename), content, opts.force, result);
909
+ ensureDir(dirname(join5(templatesDir, filename)));
910
+ writeFile(join5(templatesDir, filename), content, opts.force, result);
817
911
  }
818
- const claudeMdPath = join4(targetDir, "CLAUDE.md");
819
- if (existsSync4(claudeMdPath) && !opts.force) {
912
+ const claudeMdPath = join5(targetDir, "CLAUDE.md");
913
+ if (existsSync5(claudeMdPath) && !opts.force) {
820
914
  result.skipped.push(claudeMdPath);
821
915
  } else {
822
916
  const projectName = basename(targetDir);
823
917
  const content = generateCLAUDEMd(projectName, stack, existingSkills, {
824
918
  privateProfile: gitignoreProfile === "private"
825
919
  });
826
- writeFileSync2(claudeMdPath, content, "utf-8");
920
+ writeFileSync3(claudeMdPath, content, "utf-8");
827
921
  result.created.push(claudeMdPath);
828
922
  }
829
- const agentsMdPath = join4(targetDir, "AGENTS.md");
830
- if (existsSync4(agentsMdPath) && !opts.force) {
923
+ const agentsMdPath = join5(targetDir, "AGENTS.md");
924
+ if (existsSync5(agentsMdPath) && !opts.force) {
831
925
  result.skipped.push(agentsMdPath);
832
926
  } else {
833
927
  const projectName = basename(targetDir);
834
928
  const content = generateAgentsMd(projectName, stack, gitignoreProfile === "private");
835
- writeFileSync2(agentsMdPath, content, "utf-8");
929
+ writeFileSync3(agentsMdPath, content, "utf-8");
836
930
  result.created.push(agentsMdPath);
837
931
  }
838
932
  const fileHashes = {};
839
933
  if (wants("claude")) {
840
934
  for (const [filename, content] of Object.entries(SKILLS)) {
841
935
  const skillName = filename.replace(/\.md$/, "");
842
- fileHashes[join4(".claude", "skills", skillName, "SKILL.md")] = hashContent(content);
936
+ fileHashes[join5(".claude", "skills", skillName, "SKILL.md")] = hashContent(content);
843
937
  }
844
938
  }
845
939
  if (wants("codex")) {
846
940
  for (const [filename, content] of Object.entries(CODEX_SKILLS)) {
847
941
  const skillName = filename.replace(/\.md$/, "");
848
- fileHashes[join4(".agents", "skills", skillName, "SKILL.md")] = hashContent(content);
942
+ fileHashes[join5(".agents", "skills", skillName, "SKILL.md")] = hashContent(content);
849
943
  }
850
944
  }
851
945
  for (const [filename, content] of Object.entries(TEMPLATES)) {
852
- fileHashes[join4("docs", "templates", filename)] = hashContent(content);
946
+ fileHashes[join5("docs", "templates", filename)] = hashContent(content);
853
947
  }
854
948
  if (wants("pi")) {
855
949
  for (const [filename, content] of Object.entries(PI_SKILLS)) {
856
950
  const skillName = filename.replace(/\.md$/, "");
857
- fileHashes[join4(".pi", "skills", skillName, "SKILL.md")] = hashContent(content);
951
+ fileHashes[join5(".pi", "skills", skillName, "SKILL.md")] = hashContent(content);
858
952
  }
859
953
  for (const [name, content] of Object.entries(PI_SCRIPTS)) {
860
- fileHashes[join4(".pi", "scripts", "joycraft", name)] = hashContent(content);
954
+ fileHashes[join5(".pi", "scripts", "joycraft", name)] = hashContent(content);
861
955
  }
862
956
  for (const [name, content] of Object.entries(PI_EXTENSIONS)) {
863
- fileHashes[join4(".pi", "extensions", name)] = hashContent(content);
957
+ fileHashes[join5(".pi", "extensions", name)] = hashContent(content);
864
958
  }
865
959
  for (const [name, content] of Object.entries(PI_AGENTS)) {
866
- fileHashes[join4(".pi", "agents", name)] = hashContent(content);
960
+ fileHashes[join5(".pi", "agents", name)] = hashContent(content);
867
961
  }
868
962
  }
869
963
  writeVersion(targetDir, getPackageVersion(), fileHashes, gitignoreProfile, harnesses);
870
964
  applyGitignoreProfile(targetDir, gitignoreProfile);
965
+ if (wants("pi")) {
966
+ const outcome = ensurePiExcludedFromTsconfig(targetDir);
967
+ if (outcome.status === "added") {
968
+ result.modified.push(outcome.path);
969
+ result.warnings.push(
970
+ 'Added ".pi" to tsconfig.json "exclude" so the Pi extension stays out of your TypeScript build.'
971
+ );
972
+ } else if (outcome.status === "skipped") {
973
+ result.warnings.push(outcome.reason);
974
+ }
975
+ }
871
976
  if (wants("claude")) {
872
- const hooksDir = join4(targetDir, ".claude", "hooks");
977
+ const hooksDir = join5(targetDir, ".claude", "hooks");
873
978
  ensureDir(hooksDir);
874
979
  const hookScript = `// Joycraft version check \u2014 runs on Claude Code session start
875
980
  import { readFileSync } from 'node:fs';
@@ -883,13 +988,13 @@ try {
883
988
  }
884
989
  } catch {}
885
990
  `;
886
- writeFile(join4(hooksDir, "joycraft-version-check.mjs"), hookScript, opts.force, result);
887
- const settingsPath = join4(targetDir, ".claude", "settings.json");
991
+ writeFile(join5(hooksDir, "joycraft-version-check.mjs"), hookScript, opts.force, result);
992
+ const settingsPath = join5(targetDir, ".claude", "settings.json");
888
993
  let settings = {};
889
994
  let settingsMalformed = false;
890
- if (existsSync4(settingsPath)) {
995
+ if (existsSync5(settingsPath)) {
891
996
  try {
892
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
997
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
893
998
  } catch {
894
999
  settingsMalformed = true;
895
1000
  result.warnings.push(
@@ -922,13 +1027,13 @@ try {
922
1027
  });
923
1028
  }
924
1029
  if (!hasJoycraftHook || envMissing) {
925
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1030
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
926
1031
  if (!result.created.includes(settingsPath)) result.created.push(settingsPath);
927
1032
  }
928
1033
  const permissions = generatePermissions(stack);
929
- if (existsSync4(settingsPath)) {
1034
+ if (existsSync5(settingsPath)) {
930
1035
  try {
931
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1036
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
932
1037
  } catch {
933
1038
  result.warnings.push(
934
1039
  "settings.json became unreadable after hook merge \u2014 skipping permissions merge.\n Fix the JSON in .claude/settings.json and re-run init."
@@ -947,7 +1052,7 @@ try {
947
1052
  for (const rule of permissions.deny) {
948
1053
  if (!perms.deny.includes(rule)) perms.deny.push(rule);
949
1054
  }
950
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1055
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
951
1056
  }
952
1057
  }
953
1058
  const hookResult = installSafeguardHooks(targetDir, [], opts.force, settingsMalformed);
@@ -955,9 +1060,9 @@ try {
955
1060
  result.skipped.push(...hookResult.skipped);
956
1061
  }
957
1062
  if (gitignoreProfile === "shared" && wants("claude")) {
958
- const gitignorePath = join4(targetDir, ".gitignore");
959
- if (existsSync4(gitignorePath)) {
960
- const gitignore = readFileSync3(gitignorePath, "utf-8");
1063
+ const gitignorePath = join5(targetDir, ".gitignore");
1064
+ if (existsSync5(gitignorePath)) {
1065
+ const gitignore = readFileSync4(gitignorePath, "utf-8");
961
1066
  if (/^\.claude\/?$/m.test(gitignore) || /^\.claude\/\*$/m.test(gitignore)) {
962
1067
  result.warnings.push(
963
1068
  ".claude/ is in your .gitignore \u2014 teammates won't get Joycraft skills.\n Add this line to .gitignore to fix: !.claude/skills/"
@@ -1041,4 +1146,4 @@ function printSummary(result, stack, existingSkills = [], isPi = false, gitignor
1041
1146
  export {
1042
1147
  init
1043
1148
  };
1044
- //# sourceMappingURL=init-LXSMLAY5.js.map
1149
+ //# sourceMappingURL=init-OO77DURX.js.map