@ikon85/agent-workflow-kit 0.10.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.
Files changed (273) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +139 -0
  2. package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  3. package/.agents/skills/audit-skills/SKILL.md +145 -0
  4. package/.agents/skills/board-to-waves/SKILL.md +135 -0
  5. package/.agents/skills/code-review/SKILL.md +64 -0
  6. package/.agents/skills/codebase-design/DEEPENING.md +37 -0
  7. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  8. package/.agents/skills/codebase-design/SKILL.md +114 -0
  9. package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  10. package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
  11. package/.agents/skills/decision-gate/SKILL.md +67 -0
  12. package/.agents/skills/diagnose/SKILL.md +136 -0
  13. package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  14. package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  15. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  16. package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  17. package/.agents/skills/domain-modeling/SKILL.md +76 -0
  18. package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  19. package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
  20. package/.agents/skills/grill-me/SKILL.md +54 -0
  21. package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  22. package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  23. package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  24. package/.agents/skills/grill-with-docs/SKILL.md +137 -0
  25. package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  26. package/.agents/skills/implement/SKILL.md +15 -0
  27. package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  28. package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  29. package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  30. package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  31. package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  32. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  33. package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  34. package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
  35. package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  36. package/.agents/skills/kit-release/SKILL.md +56 -0
  37. package/.agents/skills/local-ci/SKILL.md +94 -0
  38. package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
  39. package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
  40. package/.agents/skills/prototype/LOGIC.md +79 -0
  41. package/.agents/skills/prototype/SKILL.md +30 -0
  42. package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  43. package/.agents/skills/prototype/UI.md +112 -0
  44. package/.agents/skills/research/SKILL.md +12 -0
  45. package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  46. package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
  47. package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  48. package/.agents/skills/retro/SKILL.md +209 -0
  49. package/.agents/skills/scale-check/SKILL.md +141 -0
  50. package/.agents/skills/security-audit/SKILL.md +67 -0
  51. package/.agents/skills/setup-workflow/SKILL.md +187 -0
  52. package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  53. package/.agents/skills/setup-workflow/board-sync.md +111 -0
  54. package/.agents/skills/setup-workflow/code-review.md +26 -0
  55. package/.agents/skills/setup-workflow/domain.md +51 -0
  56. package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
  57. package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  58. package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
  59. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  60. package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
  61. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  62. package/.agents/skills/setup-workflow/triage-labels.md +18 -0
  63. package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
  64. package/.agents/skills/spec-self-critique/SKILL.md +152 -0
  65. package/.agents/skills/spec-self-critique/scenarios.md +82 -0
  66. package/.agents/skills/tdd/SKILL.md +120 -0
  67. package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  68. package/.agents/skills/tdd/deep-modules.md +33 -0
  69. package/.agents/skills/tdd/interface-design.md +31 -0
  70. package/.agents/skills/tdd/mocking.md +59 -0
  71. package/.agents/skills/tdd/refactoring.md +10 -0
  72. package/.agents/skills/tdd/tests.md +61 -0
  73. package/.agents/skills/to-issues/SKILL.md +325 -0
  74. package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  75. package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  76. package/.agents/skills/to-prd/SKILL.md +180 -0
  77. package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  78. package/.agents/skills/to-waves/SKILL.md +260 -0
  79. package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  80. package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
  81. package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
  82. package/.agents/skills/triage/SKILL.md +122 -0
  83. package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  84. package/.agents/skills/verify-spike/SKILL.md +39 -0
  85. package/.agents/skills/wayfinder/SKILL.md +127 -0
  86. package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  87. package/.agents/skills/wrapup/SKILL.md +71 -0
  88. package/.agents/skills/wrapup/SOURCES.txt +6 -0
  89. package/.agents/skills/zoom-out/SKILL.md +7 -0
  90. package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  91. package/.claude/hooks/_hook_utils.py +184 -0
  92. package/.claude/hooks/drift-guard.py +176 -0
  93. package/.claude/hooks/skill-drift-hint.py +122 -0
  94. package/.claude/hooks/sync-board-status.py +191 -0
  95. package/.claude/skills/ask-matt/SKILL.md +139 -0
  96. package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  97. package/.claude/skills/audit-skills/SKILL.md +145 -0
  98. package/.claude/skills/board-to-waves/SKILL.md +148 -0
  99. package/.claude/skills/code-review/SKILL.md +64 -0
  100. package/.claude/skills/codebase-design/DEEPENING.md +37 -0
  101. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  102. package/.claude/skills/codebase-design/SKILL.md +114 -0
  103. package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  104. package/.claude/skills/codex-build/SKILL.md +126 -0
  105. package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
  106. package/.claude/skills/codex-review/SKILL.md +145 -0
  107. package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
  108. package/.claude/skills/decision-gate/SKILL.md +67 -0
  109. package/.claude/skills/diagnose/SKILL.md +136 -0
  110. package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  111. package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  112. package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
  113. package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  114. package/.claude/skills/domain-modeling/SKILL.md +76 -0
  115. package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  116. package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
  117. package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
  118. package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
  119. package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
  120. package/.claude/skills/grill-me/SKILL.md +54 -0
  121. package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  122. package/.claude/skills/grill-me-codex/SKILL.md +158 -0
  123. package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
  124. package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  125. package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  126. package/.claude/skills/grill-with-docs/SKILL.md +137 -0
  127. package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  128. package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
  129. package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
  130. package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
  131. package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
  132. package/.claude/skills/implement/SKILL.md +15 -0
  133. package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  134. package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  135. package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  136. package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  137. package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  138. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  139. package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  140. package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
  141. package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  142. package/.claude/skills/kit-release/SKILL.md +56 -0
  143. package/.claude/skills/local-ci/SKILL.md +94 -0
  144. package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
  145. package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
  146. package/.claude/skills/prototype/LOGIC.md +79 -0
  147. package/.claude/skills/prototype/SKILL.md +30 -0
  148. package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  149. package/.claude/skills/prototype/UI.md +112 -0
  150. package/.claude/skills/research/SKILL.md +12 -0
  151. package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  152. package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
  153. package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  154. package/.claude/skills/retro/SKILL.md +209 -0
  155. package/.claude/skills/scale-check/SKILL.md +141 -0
  156. package/.claude/skills/security-audit/SKILL.md +67 -0
  157. package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
  158. package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
  159. package/.claude/skills/setup-workflow/SKILL.md +187 -0
  160. package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  161. package/.claude/skills/setup-workflow/board-sync.md +111 -0
  162. package/.claude/skills/setup-workflow/code-review.md +26 -0
  163. package/.claude/skills/setup-workflow/domain.md +51 -0
  164. package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
  165. package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  166. package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
  167. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  168. package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
  169. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  170. package/.claude/skills/setup-workflow/triage-labels.md +18 -0
  171. package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
  172. package/.claude/skills/skill-manifest.json +389 -0
  173. package/.claude/skills/spec-self-critique/SKILL.md +152 -0
  174. package/.claude/skills/spec-self-critique/scenarios.md +82 -0
  175. package/.claude/skills/tdd/SKILL.md +120 -0
  176. package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  177. package/.claude/skills/tdd/deep-modules.md +33 -0
  178. package/.claude/skills/tdd/interface-design.md +31 -0
  179. package/.claude/skills/tdd/mocking.md +59 -0
  180. package/.claude/skills/tdd/refactoring.md +10 -0
  181. package/.claude/skills/tdd/tests.md +61 -0
  182. package/.claude/skills/to-issues/SKILL.md +325 -0
  183. package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  184. package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  185. package/.claude/skills/to-prd/SKILL.md +180 -0
  186. package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  187. package/.claude/skills/to-waves/SKILL.md +260 -0
  188. package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  189. package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
  190. package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
  191. package/.claude/skills/triage/SKILL.md +122 -0
  192. package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  193. package/.claude/skills/verify-spike/SKILL.md +39 -0
  194. package/.claude/skills/wayfinder/SKILL.md +127 -0
  195. package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  196. package/.claude/skills/wrapup/SKILL.md +81 -0
  197. package/.claude/skills/wrapup/SOURCES.txt +6 -0
  198. package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
  199. package/.claude/skills/write-a-skill/SKILL.md +88 -0
  200. package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
  201. package/.claude/skills/zoom-out/SKILL.md +7 -0
  202. package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  203. package/LICENSE +28 -0
  204. package/PROVENANCE.md +39 -0
  205. package/README.md +619 -0
  206. package/agent-workflow-kit.package.json +1939 -0
  207. package/docs/agents/board-sync.md +71 -0
  208. package/docs/agents/code-review.md +14 -0
  209. package/docs/agents/domain.md +7 -0
  210. package/docs/agents/issue-tracker.md +12 -0
  211. package/docs/agents/security-audit-runbook-template.md +205 -0
  212. package/docs/agents/skills/orchestrate-wave.md +13 -0
  213. package/docs/agents/skills/spec-self-critique.md +18 -0
  214. package/docs/agents/triage-labels.md +10 -0
  215. package/docs/agents/wave-anchor-template.md +88 -0
  216. package/docs/conventions/spec-completeness.md +23 -0
  217. package/docs/index.html +56 -0
  218. package/docs/methodology.html +488 -0
  219. package/docs/methodology.svg +93 -0
  220. package/docs/workflow.png +0 -0
  221. package/package.json +40 -0
  222. package/scripts/anchor_table.py +343 -0
  223. package/scripts/board-sync.py +1232 -0
  224. package/scripts/board_config.py +147 -0
  225. package/scripts/board_fields.py +192 -0
  226. package/scripts/build-kit.mjs +116 -0
  227. package/scripts/build-kit.test.mjs +59 -0
  228. package/scripts/check-kit-staleness.mjs +45 -0
  229. package/scripts/check-kit-staleness.test.mjs +19 -0
  230. package/scripts/execute-ready-check.py +516 -0
  231. package/scripts/issue_deps.py +79 -0
  232. package/scripts/kit-release.mjs +100 -0
  233. package/scripts/kit-release.test.mjs +67 -0
  234. package/scripts/lib/audit-refs.mjs +146 -0
  235. package/scripts/lib/scrub.mjs +113 -0
  236. package/scripts/lib/scrub.test.mjs +187 -0
  237. package/scripts/loc_offender_core.py +196 -0
  238. package/scripts/loc_offender_gate.py +182 -0
  239. package/scripts/node_kind.py +68 -0
  240. package/scripts/portability_profile_scan.py +53 -0
  241. package/scripts/pr-body-check.py +263 -0
  242. package/scripts/program_graph.py +125 -0
  243. package/scripts/program_graph_parse.py +275 -0
  244. package/scripts/program_graph_validate.py +256 -0
  245. package/scripts/program_sync.py +160 -0
  246. package/scripts/release-delta-guard.mjs +96 -0
  247. package/scripts/release-delta-guard.test.mjs +53 -0
  248. package/scripts/test_board_sync_create_idempotency.py +104 -0
  249. package/scripts/test_dist_kit_smoke.py +199 -0
  250. package/scripts/test_kit_docs_language_census.py +178 -0
  251. package/scripts/test_skill_code_review_seed.py +89 -0
  252. package/scripts/test_skill_frontmatter_lint.py +226 -0
  253. package/scripts/test_skill_gh_lint.py +124 -0
  254. package/scripts/test_skill_language_census.py +254 -0
  255. package/scripts/test_skill_portability_lint.py +773 -0
  256. package/scripts/test_skill_publish_audit.py +300 -0
  257. package/scripts/test_skill_selfcontainment_lint.py +255 -0
  258. package/scripts/test_skill_setup_workflow_seeds.py +200 -0
  259. package/scripts/test_skill_stale_name_lint.py +126 -0
  260. package/scripts/test_skill_surface_refs.py +300 -0
  261. package/scripts/test_skill_trailing_artifact_lint.py +71 -0
  262. package/scripts/wrapup-land.py +573 -0
  263. package/src/cli.mjs +70 -0
  264. package/src/commands/diff.mjs +6 -0
  265. package/src/commands/init.mjs +59 -0
  266. package/src/commands/uninstall.mjs +43 -0
  267. package/src/commands/update.mjs +95 -0
  268. package/src/lib/atomicWrite.mjs +49 -0
  269. package/src/lib/bundle.mjs +133 -0
  270. package/src/lib/hash.mjs +13 -0
  271. package/src/lib/manifest.mjs +58 -0
  272. package/src/lib/sentinel.mjs +20 -0
  273. package/src/lib/settings.mjs +23 -0
package/src/cli.mjs ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
4
+ import * as p from '@clack/prompts';
5
+ import { init } from './commands/init.mjs';
6
+ import { update } from './commands/update.mjs';
7
+ import { diff } from './commands/diff.mjs';
8
+ import { uninstall } from './commands/uninstall.mjs';
9
+
10
+ const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const consumerRoot = process.cwd();
12
+
13
+ function stamp() {
14
+ // backup suffix: YYYYMMDDTHHMMSS (no separators that collide with shells)
15
+ return new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
16
+ }
17
+
18
+ const args = process.argv.slice(2);
19
+ const cmd = args[0];
20
+ const force = args.includes('--force');
21
+ const yes = args.includes('--yes') || args.includes('-y');
22
+
23
+ p.intro('agent-workflow-kit');
24
+
25
+ try {
26
+ if (cmd === 'init') {
27
+ const r = await init({ kitRoot: KIT_ROOT, consumerRoot, force });
28
+ p.note(
29
+ `copied ${r.copied.length} · seeded ${r.seeded.length} stub(s)` +
30
+ (r.skipped.length ? `\nskipped (pre-existing, use --force): ${r.skipped.join(', ')}` : ''),
31
+ 'init'
32
+ );
33
+ p.outro('Next: run /setup-workflow to fill the project layer + board profile. ' +
34
+ 'To enable the drift-guard hook, add .claude/hooks/drift-guard.py to your settings.json hooks.');
35
+ } else if (cmd === 'diff') {
36
+ const r = await diff({ kitRoot: KIT_ROOT, consumerRoot });
37
+ printPlan(r);
38
+ p.outro('Dry run — nothing written. Run `update` to apply.');
39
+ } else if (cmd === 'update') {
40
+ const decide = async (_action, path) => {
41
+ if (yes) return true;
42
+ const ok = await p.confirm({ message: `Upstream removed ${path} — delete it locally?` });
43
+ return ok === true;
44
+ };
45
+ const r = await update({ kitRoot: KIT_ROOT, consumerRoot, now: stamp(), decide });
46
+ printPlan(r);
47
+ for (const c of r.conflicts) p.note(c.diff || '(binary/!text)', `conflict (backed up): ${c.path}`);
48
+ p.outro(`updated ${r.updated.length} · added ${r.added.length} · ` +
49
+ `conflicts ${r.conflicts.length} · deleted ${r.deleted.length}`);
50
+ } else if (cmd === 'uninstall') {
51
+ const ok = yes || (await p.confirm({ message: 'Remove kit-installed files?' })) === true;
52
+ if (!ok) { p.cancel('Aborted.'); process.exit(0); }
53
+ const r = await uninstall({ consumerRoot });
54
+ p.outro(`removed ${r.removed.length} · retained (edited/referenced) ${r.retained.length}`);
55
+ } else {
56
+ p.note('Usage: agent-workflow-kit <init|update|diff|uninstall> [--force] [--yes]');
57
+ p.outro('');
58
+ }
59
+ } catch (err) {
60
+ p.cancel(`Error: ${err.message}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ function printPlan(r) {
65
+ const lines = [];
66
+ for (const k of ['added', 'updated', 'userModified', 'unchanged', 'deleted', 'keptDeleted'])
67
+ if (r[k]?.length) lines.push(`${k}: ${r[k].length}`);
68
+ if (r.conflicts?.length) lines.push(`conflicts: ${r.conflicts.length}`);
69
+ p.note(lines.join('\n') || 'no changes', 'plan');
70
+ }
@@ -0,0 +1,6 @@
1
+ import { update } from './update.mjs';
2
+
3
+ /** Dry-run of `update`: classifies what an update would do, writes nothing. */
4
+ export async function diff({ kitRoot, consumerRoot }) {
5
+ return update({ kitRoot, consumerRoot, now: 'dry', dryRun: true });
6
+ }
@@ -0,0 +1,59 @@
1
+ import { readFile, access } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { sha256File } from '../lib/hash.mjs';
4
+ import { writeAtomic } from '../lib/atomicWrite.mjs';
5
+ import { stubSentinel } from '../lib/sentinel.mjs';
6
+ import { STUB_TARGETS } from '../lib/bundle.mjs';
7
+ import {
8
+ readManifest, writeManifest, emptyConsumerManifest,
9
+ PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME,
10
+ } from '../lib/manifest.mjs';
11
+
12
+ const exists = (p) => access(p).then(() => true, () => false);
13
+
14
+ /**
15
+ * Install the kit into a consumer repo.
16
+ * - copies every package-manifest file (never-clobbering a pre-existing UNTRACKED
17
+ * file unless `force`),
18
+ * - writes the consumer manifest,
19
+ * - seeds the doc-layer stubs with the setup-workflow sentinel (skips any that
20
+ * already exist — idempotent),
21
+ * - never touches board-sync.md / CLAUDE.md / AGENTS.md.
22
+ */
23
+ export async function init({ kitRoot, consumerRoot, force = false }) {
24
+ const pkg = await readManifest(join(kitRoot, PACKAGE_MANIFEST_NAME));
25
+ if (!pkg) throw new Error('kit package manifest not found');
26
+ const prior = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
27
+ const tracked = new Set((prior?.installed ?? []).map((e) => e.path));
28
+
29
+ const result = { copied: [], skipped: [], seeded: [] };
30
+ const installed = [];
31
+
32
+ for (const f of pkg.files) {
33
+ const dest = join(consumerRoot, f.path);
34
+ if (await exists(dest) && !tracked.has(f.path) && !force) {
35
+ result.skipped.push(f.path); // pre-existing untracked → never-clobber
36
+ continue;
37
+ }
38
+ await writeAtomic(dest, await readFile(join(kitRoot, f.path)), f.mode);
39
+ installed.push({
40
+ path: f.path, kind: f.kind, ownerSkill: f.ownerSkill, surface: f.surface,
41
+ installedSha256: await sha256File(dest), origin: 'kit',
42
+ });
43
+ result.copied.push(f.path);
44
+ }
45
+
46
+ await writeManifest(
47
+ join(consumerRoot, CONSUMER_MANIFEST_NAME),
48
+ { ...emptyConsumerManifest(pkg.kitVersion), installed }
49
+ );
50
+
51
+ for (const stub of STUB_TARGETS) {
52
+ const dest = join(consumerRoot, stub);
53
+ if (await exists(dest)) continue; // already present (stub or filled) → leave it
54
+ await writeAtomic(dest, stubSentinel() + '\n');
55
+ result.seeded.push(stub);
56
+ }
57
+
58
+ return result;
59
+ }
@@ -0,0 +1,43 @@
1
+ import { access, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { sha256File } from '../lib/hash.mjs';
4
+ import { hookReferenced } from '../lib/settings.mjs';
5
+ import { writeManifest, readManifest, emptyConsumerManifest, CONSUMER_MANIFEST_NAME } from '../lib/manifest.mjs';
6
+
7
+ const exists = (p) => access(p).then(() => true, () => false);
8
+
9
+ /**
10
+ * Remove kit-installed files. User-edited files and hook files still referenced
11
+ * by settings are retained (Codex R1#11 / R3#7). If anything is retained, the
12
+ * manifest is kept with those entries marked `orphanedByUninstall`; on a fully
13
+ * clean uninstall the manifest is removed.
14
+ */
15
+ export async function uninstall({ consumerRoot }) {
16
+ const consumer = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
17
+ if (!consumer) throw new Error('not initialised — nothing to uninstall');
18
+
19
+ const res = { removed: [], retained: [] };
20
+ const retainedEntries = [];
21
+
22
+ for (const e of consumer.installed) {
23
+ const dest = join(consumerRoot, e.path);
24
+ if (!(await exists(dest))) continue; // already gone
25
+ const userEdited = (await sha256File(dest)) !== e.installedSha256;
26
+ const referenced = e.kind === 'hook' && (await hookReferenced(consumerRoot, e.path));
27
+ if (userEdited || referenced) {
28
+ retainedEntries.push({ ...e, orphanedByUninstall: true });
29
+ res.retained.push(e.path);
30
+ } else {
31
+ await rm(dest);
32
+ res.removed.push(e.path);
33
+ }
34
+ }
35
+
36
+ const manifestPath = join(consumerRoot, CONSUMER_MANIFEST_NAME);
37
+ if (retainedEntries.length) {
38
+ await writeManifest(manifestPath, { ...emptyConsumerManifest(consumer.kitVersion), installed: retainedEntries });
39
+ } else {
40
+ await rm(manifestPath, { force: true });
41
+ }
42
+ return res;
43
+ }
@@ -0,0 +1,95 @@
1
+ import { readFile, access, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { sha256File } from '../lib/hash.mjs';
4
+ import { writeAtomic, backupFile, lineDiff } from '../lib/atomicWrite.mjs';
5
+ import { hookReferenced } from '../lib/settings.mjs';
6
+ import {
7
+ readManifest, writeManifest, emptyConsumerManifest,
8
+ PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME, indexByPath,
9
+ } from '../lib/manifest.mjs';
10
+
11
+ const exists = (p) => access(p).then(() => true, () => false);
12
+
13
+ /**
14
+ * Reconcile an installed consumer against the current kit (three-way: installed
15
+ * hash vs current file = user edit?, vs package desired = upstream change?).
16
+ * - unmodified + upstream changed → atomic overwrite (`updated`)
17
+ * - user-edited + upstream changed → timestamped backup + diff, NOT clobbered (`conflicts`)
18
+ * - user-edited + upstream same → left as-is (`userModified`)
19
+ * - new upstream file → installed (`added`)
20
+ * - upstream-removed + unmodified → offered for deletion via `decide` (`deleted`/`keptDeleted`)
21
+ * - a hook file referenced by the consumer's settings is never auto-deleted (R3#7)
22
+ * `dryRun` classifies without writing (powers `diff`). `now` = backup stamp.
23
+ */
24
+ export async function update({ kitRoot, consumerRoot, now, decide = () => false, dryRun = false }) {
25
+ const pkg = await readManifest(join(kitRoot, PACKAGE_MANIFEST_NAME));
26
+ if (!pkg) throw new Error('kit package manifest not found');
27
+ const consumer = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
28
+ if (!consumer) throw new Error('not initialised — run `init` first');
29
+
30
+ const installedIdx = indexByPath(consumer, 'installed');
31
+ const pkgIdx = indexByPath(pkg, 'files');
32
+ const res = { unchanged: [], updated: [], conflicts: [], userModified: [], added: [], deleted: [], keptDeleted: [] };
33
+ const nextInstalled = [];
34
+
35
+ for (const f of pkg.files) {
36
+ const dest = join(consumerRoot, f.path);
37
+ const prior = installedIdx.get(f.path);
38
+ const cur = (await exists(dest)) ? await sha256File(dest) : null;
39
+
40
+ if (!prior || cur === null) {
41
+ if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, f.path)), f.mode);
42
+ nextInstalled.push(entry(f, f.sha256));
43
+ res.added.push(f.path);
44
+ continue;
45
+ }
46
+ const userEdited = cur !== prior.installedSha256;
47
+ const upstreamChanged = f.sha256 !== prior.installedSha256;
48
+
49
+ if (!userEdited && upstreamChanged) {
50
+ if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, f.path)), f.mode);
51
+ nextInstalled.push(entry(f, f.sha256));
52
+ res.updated.push(f.path);
53
+ } else if (userEdited && upstreamChanged) {
54
+ const incoming = await readFile(join(kitRoot, f.path), 'utf8');
55
+ const diff = lineDiff(await readFile(dest, 'utf8'), incoming);
56
+ if (!dryRun) await backupFile(dest, now);
57
+ nextInstalled.push(prior); // keep installed-hash baseline; file stays user's
58
+ res.conflicts.push({ path: f.path, diff });
59
+ } else if (userEdited) {
60
+ nextInstalled.push(prior);
61
+ res.userModified.push(f.path);
62
+ } else {
63
+ nextInstalled.push(prior);
64
+ res.unchanged.push(f.path);
65
+ }
66
+ }
67
+
68
+ // upstream-removed files (in consumer manifest, gone from package)
69
+ for (const prior of consumer.installed) {
70
+ if (pkgIdx.has(prior.path)) continue;
71
+ const dest = join(consumerRoot, prior.path);
72
+ const cur = (await exists(dest)) ? await sha256File(dest) : null;
73
+ const userEdited = cur !== null && cur !== prior.installedSha256;
74
+ const referenced = prior.kind === 'hook' && (await hookReferenced(consumerRoot, prior.path));
75
+ if (userEdited || referenced || !(await decide('delete', prior.path))) {
76
+ nextInstalled.push(prior);
77
+ res.keptDeleted.push(prior.path);
78
+ } else {
79
+ if (!dryRun && cur !== null) await rm(dest);
80
+ res.deleted.push(prior.path);
81
+ }
82
+ }
83
+
84
+ if (!dryRun) {
85
+ await writeManifest(
86
+ join(consumerRoot, CONSUMER_MANIFEST_NAME),
87
+ { ...emptyConsumerManifest(pkg.kitVersion), installed: nextInstalled }
88
+ );
89
+ }
90
+ return res;
91
+ }
92
+
93
+ function entry(f, sha) {
94
+ return { path: f.path, kind: f.kind, ownerSkill: f.ownerSkill, surface: f.surface, installedSha256: sha, origin: 'kit' };
95
+ }
@@ -0,0 +1,49 @@
1
+ import { writeFile, rename, mkdir, copyFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+
4
+ /**
5
+ * Write `content` to `path` atomically: write a sibling temp file, then rename
6
+ * (atomic on the same filesystem). Creates parent dirs. Preserves `mode` if given.
7
+ */
8
+ export async function writeAtomic(path, content, mode) {
9
+ await mkdir(dirname(path), { recursive: true });
10
+ const tmp = `${path}.tmp-${process.pid}`;
11
+ const opts = mode === undefined ? 'utf8' : { encoding: 'utf8', mode };
12
+ await writeFile(tmp, content, opts);
13
+ await rename(tmp, path);
14
+ }
15
+
16
+ /**
17
+ * Copy an existing file to `path.<stamp>.bak` (caller supplies the stamp so the
18
+ * name never collides and tests stay deterministic). Returns the backup path.
19
+ */
20
+ export async function backupFile(path, stamp) {
21
+ const bak = `${path}.${stamp}.bak`;
22
+ await copyFile(path, bak);
23
+ return bak;
24
+ }
25
+
26
+ /**
27
+ * Minimal LCS-based line diff → unified-ish text. Context lines prefixed ' ',
28
+ * removals '-', additions '+'. Empty string when the inputs are identical.
29
+ */
30
+ export function lineDiff(oldText, newText) {
31
+ const a = oldText.split('\n');
32
+ const b = newText.split('\n');
33
+ // LCS table
34
+ const m = a.length, n = b.length;
35
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
36
+ for (let i = m - 1; i >= 0; i--)
37
+ for (let j = n - 1; j >= 0; j--)
38
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
39
+ const out = [];
40
+ let i = 0, j = 0;
41
+ while (i < m && j < n) {
42
+ if (a[i] === b[j]) { out.push(' ' + a[i]); i++; j++; }
43
+ else if (dp[i + 1][j] >= dp[i][j + 1]) { out.push('-' + a[i]); i++; }
44
+ else { out.push('+' + b[j]); j++; }
45
+ }
46
+ while (i < m) out.push('-' + a[i++]);
47
+ while (j < n) out.push('+' + b[j++]);
48
+ return out.some((l) => l[0] !== ' ') ? out.join('\n') : '';
49
+ }
@@ -0,0 +1,133 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+
4
+ // The planning skills are not usable without their helper ecosystem (Codex R2#1).
5
+ // These ship alongside the skills. Paths are relative to the bundle/consumer root.
6
+ export const HELPER_FILES = [
7
+ // Shared profile loader imported by the three planning scripts — they read
8
+ // every board-specific value from docs/agents/board-sync.md through it, so it
9
+ // MUST ship or they are broken-on-arrival. Library (imported, not run) → 0o644.
10
+ { path: 'scripts/board_config.py', kind: 'script', mode: 0o644 },
11
+ // Pure Slices-table logic imported by board-sync.py for `anchor-sync` —
12
+ // library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
13
+ { path: 'scripts/anchor_table.py', kind: 'script', mode: 0o644 },
14
+ // Programm-Graph module for `board-sync.py validate-graph` — split by
15
+ // concern (parse/validate) across three files, all libraries (imported, not
16
+ // run) → 0o644. MUST ship together or board-sync.py ImportErrors.
17
+ { path: 'scripts/program_graph.py', kind: 'script', mode: 0o644 },
18
+ { path: 'scripts/program_graph_parse.py', kind: 'script', mode: 0o644 },
19
+ { path: 'scripts/program_graph_validate.py', kind: 'script', mode: 0o644 },
20
+ // Pure node-kind classifier imported by execute-ready-check.py —
21
+ // library (imported, not run) → 0o644. MUST ship or execute-ready-check.py ImportErrors.
22
+ { path: 'scripts/node_kind.py', kind: 'script', mode: 0o644 },
23
+ // stamp-batch / field-value / promote-guard logic for board-sync.py —
24
+ // library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
25
+ { path: 'scripts/board_fields.py', kind: 'script', mode: 0o644 },
26
+ // Wellenplan Status-resync for board-sync.py's `program-sync` —
27
+ // library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
28
+ { path: 'scripts/program_sync.py', kind: 'script', mode: 0o644 },
29
+ // Blocked-by body-mirror logic for the native issue-dependency commands
30
+ // — imported by BOTH board-sync.py (dep-add/dep-remove) and
31
+ // execute-ready-check.py (drift check). Library (imported, not run) → 0o644.
32
+ // MUST ship or both ImportError on arrival.
33
+ { path: 'scripts/issue_deps.py', kind: 'script', mode: 0o644 },
34
+ { path: 'scripts/board-sync.py', kind: 'script', mode: 0o755 },
35
+ { path: 'scripts/execute-ready-check.py', kind: 'script', mode: 0o755 },
36
+ { path: 'scripts/pr-body-check.py', kind: 'script', mode: 0o755 },
37
+ // Mechanical executor for /wrapup (preflight/commit/land) — replaced the
38
+ // Sonnet phase-2 subagent. Imports board_config + anchor_table
39
+ // (both shipped above). Invokable CLI → 0o755.
40
+ { path: 'scripts/wrapup-land.py', kind: 'script', mode: 0o755 },
41
+ // Deterministic release preparation and its manifest-derived local/CI guard.
42
+ // Both are invoked through package scripts by /kit-release.
43
+ { path: 'scripts/kit-release.mjs', kind: 'script', mode: 0o644 },
44
+ { path: 'scripts/release-delta-guard.mjs', kind: 'script', mode: 0o644 },
45
+ // Shared hook utility imported by the shipped hooks (drift-guard,
46
+ // sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
47
+ // hooks ImportError on arrival.
48
+ { path: '.claude/hooks/_hook_utils.py', kind: 'hook', mode: 0o644 },
49
+ { path: '.claude/hooks/drift-guard.py', kind: 'hook', mode: 0o755 },
50
+ // SessionStart skill-freshness drift-hint (audit-skills names it). For each
51
+ // <skill>/SOURCES.txt it flags sources newer in git than the SKILL.md. Imports
52
+ // _hook_utils (shipped above); stdlib-only otherwise. Executable hook → 0o755.
53
+ { path: '.claude/hooks/skill-drift-hint.py', kind: 'hook', mode: 0o755 },
54
+ // Board-status pickup hook — profile-driven (reads project/field/status ids
55
+ // from the consumer-seeded board profile), so it ships portably. /tdd names it.
56
+ { path: '.claude/hooks/sync-board-status.py', kind: 'hook', mode: 0o755 },
57
+ { path: 'docs/agents/wave-anchor-template.md', kind: 'template', mode: 0o644 },
58
+ // Part-0–5 security-audit runbook skeleton (the security-audit skill names it).
59
+ // The stack-coupled checklist is deliberately NOT shipped — this template is the
60
+ // generic structure a consumer copies + fills stack-specifically. Prose → 0o644.
61
+ { path: 'docs/agents/security-audit-runbook-template.md', kind: 'template', mode: 0o644 },
62
+ // Opt-in LoC-offender drive gate (setup-workflow §7b names it). Both are
63
+ // stdlib-only and profile-driven (threshold + offenders read from the
64
+ // consumer-seeded max-lines-allowlist.json), so they ship portably. The gate
65
+ // imports the core, so the core MUST ship too. core = library → 0o644;
66
+ // gate = invokable CLI (pre-push / manual) → 0o755.
67
+ { path: 'scripts/loc_offender_core.py', kind: 'script', mode: 0o644 },
68
+ { path: 'scripts/loc_offender_gate.py', kind: 'script', mode: 0o755 },
69
+ ];
70
+
71
+ // Project-layer docs `init` seeds as empty stubs (sentinel first line). NOT
72
+ // board-sync.md — that is discovery-dependent, /setup-workflow classifies+fills it
73
+ // (Codex R1#8).
74
+ export const STUB_TARGETS = [
75
+ 'docs/agents/issue-tracker.md',
76
+ 'docs/agents/triage-labels.md',
77
+ 'docs/agents/domain.md',
78
+ 'docs/agents/skills/spec-self-critique.md',
79
+ 'docs/agents/skills/orchestrate-wave.md',
80
+ 'docs/agents/skills/local-ci.md',
81
+ 'docs/agents/skills/git-worktree-recover.md',
82
+ 'docs/agents/skills/audit-skills.md',
83
+ 'docs/agents/skills/security-audit.md',
84
+ 'docs/conventions/spec-completeness.md',
85
+ ];
86
+
87
+ const SURFACE_DIR = { claude: '.claude/skills', codex: '.agents/skills' };
88
+
89
+ /** Skills with publish:true, each with the surfaces it installs into. */
90
+ export function publishableSkills(manifest) {
91
+ return Object.entries(manifest.skills)
92
+ .filter(([, e]) => e.publish)
93
+ .map(([name, e]) => ({ name, surfaces: e.surfaces }));
94
+ }
95
+
96
+ async function walk(dir) {
97
+ const out = [];
98
+ let entries;
99
+ try {
100
+ entries = await readdir(dir, { withFileTypes: true });
101
+ } catch (err) {
102
+ if (err.code === 'ENOENT') return out;
103
+ throw err;
104
+ }
105
+ for (const e of entries) {
106
+ const p = join(dir, e.name);
107
+ if (e.isDirectory()) out.push(...(await walk(p)));
108
+ else if (e.isFile()) out.push(p);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Every file to copy from the kit into a consumer, plus the doc stubs to seed.
115
+ * Returns { files: [{ src, dest, kind, ownerSkill?, surface?, mode }], stubs: [paths] }.
116
+ * `src`/`dest` are repo-root-relative (same layout in kit and consumer).
117
+ */
118
+ export async function collectBundle(repoRoot, manifest) {
119
+ const files = [];
120
+ for (const { name, surfaces } of publishableSkills(manifest)) {
121
+ for (const surface of surfaces) {
122
+ const base = join(SURFACE_DIR[surface], name);
123
+ for (const abs of await walk(join(repoRoot, base))) {
124
+ const rel = relative(repoRoot, abs);
125
+ files.push({ src: rel, dest: rel, kind: 'skill', ownerSkill: name, surface, mode: 0o644 });
126
+ }
127
+ }
128
+ }
129
+ for (const h of HELPER_FILES) {
130
+ files.push({ src: h.path, dest: h.path, kind: h.kind, mode: h.mode });
131
+ }
132
+ return { files, stubs: STUB_TARGETS };
133
+ }
@@ -0,0 +1,13 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+
4
+ /** sha256 hex digest of a file's raw bytes. */
5
+ export async function sha256File(path) {
6
+ const buf = await readFile(path);
7
+ return createHash('sha256').update(buf).digest('hex');
8
+ }
9
+
10
+ /** sha256 hex digest of an in-memory string/buffer. */
11
+ export function sha256(content) {
12
+ return createHash('sha256').update(content).digest('hex');
13
+ }
@@ -0,0 +1,58 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { writeAtomic } from './atomicWrite.mjs';
3
+
4
+ // Two manifests (Codex R1#9 / R3#1):
5
+ // - package manifest (shipped with the kit): the desired-state file list.
6
+ // - consumer manifest (agent-workflow-kit.json in the target repo root): installed state.
7
+ // Both model every file kind:
8
+ // { path, kind: 'skill'|'script'|'hook'|'template'|'doc', ownerSkill?, surface?,
9
+ // sha256, mode, origin: 'kit' }
10
+
11
+ export const CONSUMER_MANIFEST_NAME = 'agent-workflow-kit.json';
12
+ export const PACKAGE_MANIFEST_NAME = 'agent-workflow-kit.package.json';
13
+
14
+ /**
15
+ * Parse a JSON manifest, or null if the file does not exist. A corrupt file
16
+ * (invalid JSON — e.g. from an aborted write before writeManifest went atomic)
17
+ * throws a clear, recovery-hinting error instead of a raw JSON.parse stack;
18
+ * every command reads this file on startup, so an unreadable manifest bricks
19
+ * init/update/diff/uninstall until the user is told how to recover.
20
+ */
21
+ export async function readManifest(path) {
22
+ let raw;
23
+ try {
24
+ raw = await readFile(path, 'utf8');
25
+ } catch (err) {
26
+ if (err.code === 'ENOENT') return null;
27
+ throw err;
28
+ }
29
+ try {
30
+ return JSON.parse(raw);
31
+ } catch (err) {
32
+ throw new Error(
33
+ `${path} is corrupt (invalid JSON) and can't be read. ` +
34
+ 'Restore it from a nearby ".bak" backup, or delete it and re-run `init` to re-track the kit files.',
35
+ { cause: err }
36
+ );
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Write a manifest as pretty JSON with a trailing newline, atomically
42
+ * (temp file + rename via writeAtomic) — an abort mid-write must never leave a
43
+ * truncated/corrupt manifest, since every command reads it on startup.
44
+ */
45
+ export async function writeManifest(path, obj) {
46
+ await writeAtomic(path, JSON.stringify(obj, null, 2) + '\n');
47
+ }
48
+
49
+ export function emptyConsumerManifest(kitVersion) {
50
+ return { kitVersion, installed: [] };
51
+ }
52
+
53
+ /** Map a manifest's file list (under `key`) by `path` for quick lookup. */
54
+ export function indexByPath(manifest, key) {
55
+ const idx = new Map();
56
+ for (const entry of (manifest?.[key] ?? [])) idx.set(entry.path, entry);
57
+ return idx;
58
+ }
@@ -0,0 +1,20 @@
1
+ // setup-workflow idempotency sentinel (contract). npx init seeds project-layer
2
+ // stub files with this exact first line so /setup-workflow's stub→fill detection works.
3
+
4
+ const RE = /^<!--\s*setup-workflow:\s*state=([a-z-]+)(?:;[^>]*)?\s*-->\s*$/;
5
+
6
+ /** The first line npx init writes onto every seeded project-layer file. */
7
+ export function stubSentinel() {
8
+ return '<!-- setup-workflow: state=stub -->';
9
+ }
10
+
11
+ /**
12
+ * State declared by the FIRST line only (a later mention does not count, per).
13
+ * Returns 'stub' | 'filled' | 'not-applicable' | ... or null if the first line
14
+ * is not a sentinel.
15
+ */
16
+ export function firstLineState(text) {
17
+ const first = String(text).split('\n', 1)[0];
18
+ const m = RE.exec(first);
19
+ return m ? m[1] : null;
20
+ }
@@ -0,0 +1,23 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, basename } from 'node:path';
3
+
4
+ const SETTINGS = ['.claude/settings.json', '.claude/settings.local.json'];
5
+
6
+ /**
7
+ * True if any consumer settings file references the hook (by path or basename).
8
+ * Used so update/uninstall never silently removes a hook that is still wired in
9
+ * (Codex R3#7). Substring match on the raw text — robust to JSON shape.
10
+ */
11
+ export async function hookReferenced(consumerRoot, hookPath) {
12
+ const needle = basename(hookPath);
13
+ for (const s of SETTINGS) {
14
+ let raw;
15
+ try {
16
+ raw = await readFile(join(consumerRoot, s), 'utf8');
17
+ } catch {
18
+ continue;
19
+ }
20
+ if (raw.includes(hookPath) || raw.includes(needle)) return true;
21
+ }
22
+ return false;
23
+ }