@eltonssouza/development-utility-kit 1.0.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 (137) hide show
  1. package/.claude/agents/analyst.md +198 -0
  2. package/.claude/agents/backend-developer.md +126 -0
  3. package/.claude/agents/brain-keeper.md +229 -0
  4. package/.claude/agents/code-reviewer.md +181 -0
  5. package/.claude/agents/database-engineer.md +94 -0
  6. package/.claude/agents/devops-engineer.md +141 -0
  7. package/.claude/agents/frontend-developer.md +97 -0
  8. package/.claude/agents/gate-keeper.md +118 -0
  9. package/.claude/agents/migrator.md +291 -0
  10. package/.claude/agents/mobile-developer.md +80 -0
  11. package/.claude/agents/n8n-specialist.md +94 -0
  12. package/.claude/agents/product-owner.md +115 -0
  13. package/.claude/agents/qa-engineer.md +232 -0
  14. package/.claude/agents/release-engineer.md +204 -0
  15. package/.claude/agents/scaffold.md +87 -0
  16. package/.claude/agents/security-engineer.md +199 -0
  17. package/.claude/agents/sprint-runner.md +44 -0
  18. package/.claude/agents/stack-resolver.md +84 -0
  19. package/.claude/agents/tech-lead.md +182 -0
  20. package/.claude/agents/update-template.md +54 -0
  21. package/.claude/agents/ux-designer.md +118 -0
  22. package/.claude/settings.json +44 -0
  23. package/.claude/skills/README.md +332 -0
  24. package/.claude/skills/active-project/SKILL.md +129 -0
  25. package/.claude/skills/api-integration-test/SKILL.md +64 -0
  26. package/.claude/skills/auto-test-guard/SKILL.md +237 -0
  27. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  28. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  29. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  30. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  31. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  32. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  33. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  34. package/.claude/skills/brain-keeper/SKILL.md +60 -0
  35. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  36. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  37. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  38. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  39. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  40. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  41. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  42. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  43. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  44. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  45. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  46. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  47. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  48. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  49. package/.claude/skills/caveman/SKILL.md +187 -0
  50. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  51. package/.claude/skills/grill-me/SKILL.md +79 -0
  52. package/.claude/skills/honcho-memory/SKILL.md +207 -0
  53. package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
  54. package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
  55. package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
  56. package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
  57. package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
  58. package/.claude/skills/honcho-memory/package.json +32 -0
  59. package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
  60. package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
  61. package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
  62. package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
  63. package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
  64. package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
  65. package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
  66. package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
  67. package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
  68. package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
  69. package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
  70. package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
  71. package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
  72. package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
  73. package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
  74. package/.claude/skills/pair-debug/SKILL.md +288 -0
  75. package/.claude/skills/prd-ready-check/SKILL.md +58 -0
  76. package/.claude/skills/project-manager/SKILL.md +167 -0
  77. package/.claude/skills/quality-standards/SKILL.md +201 -0
  78. package/.claude/skills/quick-feature/SKILL.md +264 -0
  79. package/.claude/skills/run-sprint/SKILL.md +342 -0
  80. package/.claude/skills/scaffold/SKILL.md +58 -0
  81. package/.claude/skills/stack-discovery/SKILL.md +159 -0
  82. package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
  83. package/.claude/skills/to-issues/SKILL.md +163 -0
  84. package/.claude/skills/to-prd/SKILL.md +130 -0
  85. package/.claude/skills/update-template/SKILL.md +254 -0
  86. package/.claude/stacks/CODEOWNERS +30 -0
  87. package/.claude/stacks/README.md +88 -0
  88. package/.claude/stacks/_template.md +116 -0
  89. package/.claude/stacks/java/spring-boot-3.md +376 -0
  90. package/.claude/stacks/java/spring-boot-4.md +438 -0
  91. package/.claude/stacks/typescript/angular-18.md +420 -0
  92. package/.claude/stacks/typescript/angular-19.md +397 -0
  93. package/.claude/stacks/typescript/angular-21.md +494 -0
  94. package/CLAUDE.md +453 -0
  95. package/README.md +391 -0
  96. package/bin/cli.js +773 -0
  97. package/bin/lib/backup.js +62 -0
  98. package/bin/lib/detect-stack.js +476 -0
  99. package/bin/lib/help.js +233 -0
  100. package/bin/lib/identity.js +108 -0
  101. package/bin/lib/local-dir.js +69 -0
  102. package/bin/lib/manifest.js +236 -0
  103. package/bin/lib/sync-all.js +394 -0
  104. package/bin/lib/version-check.js +398 -0
  105. package/dashboard/db.js +199 -0
  106. package/dashboard/package.json +22 -0
  107. package/dashboard/public/app.js +709 -0
  108. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  109. package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
  110. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  111. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  112. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  113. package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
  114. package/dashboard/public/content/docs/pipeline.en.md +400 -0
  115. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  116. package/dashboard/public/content/docs/skills-reference.en.md +500 -0
  117. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  118. package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
  119. package/dashboard/public/content/manifest.json +102 -0
  120. package/dashboard/public/content/manual/backend.en.md +1138 -0
  121. package/dashboard/public/content/manual/existing-project.en.md +831 -0
  122. package/dashboard/public/content/manual/frontend.en.md +1065 -0
  123. package/dashboard/public/content/manual/fullstack.en.md +1508 -0
  124. package/dashboard/public/content/manual/mobile.en.md +866 -0
  125. package/dashboard/public/index.html +108 -0
  126. package/dashboard/public/style.css +610 -0
  127. package/dashboard/public/vendor/marked.min.js +69 -0
  128. package/dashboard/rtk.js +143 -0
  129. package/dashboard/server-app.js +403 -0
  130. package/dashboard/server.js +104 -0
  131. package/dashboard/test/sprint1.test.js +406 -0
  132. package/dashboard/test/sprint2.test.js +571 -0
  133. package/dashboard/test/sprint3.test.js +560 -0
  134. package/package.json +33 -0
  135. package/scripts/hooks/subagent-telemetry.sh +14 -0
  136. package/scripts/hooks/telemetry-writer.js +250 -0
  137. package/scripts/latest-versions.json +56 -0
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Help text for the duk CLI.
5
+ *
6
+ * Three modes:
7
+ * - printGeneralHelp() → full usage page (`duk help`)
8
+ * - printCommandHelp(cmd) → detailed help for one command
9
+ * - printShortHelp() → terse usage shown when called with no args
10
+ */
11
+
12
+ const GENERAL_HELP = `\
13
+ duk — development-utility-kit CLI
14
+
15
+ Usage: duk <command> [options]
16
+
17
+ COMMANDS
18
+ new <name> Scaffold new project + open Cowork for conversational stack discovery
19
+ install Inject harness into CWD (idempotent). Detects existing stack
20
+ update Alias for install
21
+ sync-all <dir> Update harness in multiple projects under <dir>. Default dry-run
22
+ dashboard Start local telemetry dashboard (Express + Chart.js)
23
+ help [command] Show this message, or detailed help for <command>
24
+
25
+ COMMON OPTIONS
26
+ --help, -h Show help (this or command-specific)
27
+ --version, -v Show duk version
28
+
29
+ COMMAND-SPECIFIC OPTIONS
30
+
31
+ install / update:
32
+ --sub <dir> Install in subdirectory instead of CWD
33
+ --dry-run Print actions without writing
34
+ --check-only Show "local vs latest harness", do not update
35
+ --force Skip drift detection (overwrite local modifications)
36
+
37
+ sync-all:
38
+ --apply Execute updates (default: dry-run)
39
+ --filter <expr> Filter projects (repeat for AND logic):
40
+ stack:<lang|framework> e.g. stack:java
41
+ type:<value> e.g. type:fullstack
42
+ age:<duration> e.g. age:30d, age:6m
43
+ harness-version:<semver> e.g. harness-version:<0.2
44
+ --exclude <name> Exclude project by folder name (repeat)
45
+
46
+ dashboard:
47
+ --port <n> Port to bind (default: 4242)
48
+ --no-open Do not open browser automatically
49
+
50
+ EXAMPLES
51
+ duk new my-app
52
+ duk install
53
+ duk install --check-only
54
+ duk sync-all C:\\development\\source\\projects --filter stack:java --apply
55
+ duk dashboard --port 4243
56
+
57
+ Run \`duk help <command>\` for detailed information.`;
58
+
59
+ const SHORT_HELP = `\
60
+ duk — development-utility-kit CLI
61
+
62
+ Usage: duk <command> [options]
63
+
64
+ Commands: new | install | update | sync-all | dashboard | help
65
+
66
+ Run \`duk help\` for full usage, or \`duk help <command>\` for command details.`;
67
+
68
+ const COMMAND_HELP = {
69
+ new: `\
70
+ duk new <name> — Scaffold a new project
71
+
72
+ Creates a folder <name> in the CWD, initialises a git repo, writes a
73
+ CLAUDE.md template with an empty Project Identity, and injects the
74
+ harness (.claude/, CLAUDE.md, HARNESS_VERSION, MANIFEST, local/).
75
+
76
+ Usage:
77
+ duk new <name>
78
+
79
+ Arguments:
80
+ <name> Project folder name (kebab-case recommended).
81
+ Must not already exist.
82
+
83
+ After running:
84
+ 1. cd <name>
85
+ 2. Open Cowork or Claude Code in the new folder
86
+ 3. First message: "sabatina pra projeto novo"
87
+ → stack-discovery skill walks you through 8 questions and fills
88
+ the Project Identity block.
89
+
90
+ Examples:
91
+ duk new my-app
92
+ duk new payment-gateway`,
93
+
94
+ install: `\
95
+ duk install — Inject the harness into a project
96
+
97
+ Detects the existing stack (pom.xml, package.json, etc.) and writes
98
+ .claude/, CLAUDE.md (preserving ## Project Identity if present),
99
+ HARNESS_VERSION, MANIFEST, and a .claude/local/ placeholder.
100
+
101
+ Usage:
102
+ duk install [options]
103
+
104
+ Options:
105
+ --sub <dir> Install into <dir> instead of the CWD.
106
+ --dry-run Print what would be done without writing.
107
+ --check-only Detect stack + report local-vs-latest harness;
108
+ do not write anything.
109
+ --force Skip drift detection (overwrite local changes).
110
+ A backup is still created.
111
+ --help, -h Show this help.
112
+
113
+ Drift detection:
114
+ install reads .claude/.MANIFEST (sha256 of every file from the prior
115
+ install). If any file was modified locally, install aborts and asks
116
+ you to either:
117
+ 1. Move customisations to .claude/local/ (never touched), or
118
+ 2. Run with --force to overwrite (backup is preserved), or
119
+ 3. Open a PR on the harness repo if the change is canonical.
120
+
121
+ Examples:
122
+ duk install
123
+ duk install --sub backend
124
+ duk install --check-only
125
+ duk install --force
126
+ duk install --dry-run`,
127
+
128
+ update: `\
129
+ duk update — Alias for "duk install"
130
+
131
+ Identical to \`duk install\`. Provided for ergonomics when you are
132
+ updating an already-adopted project.
133
+
134
+ See \`duk help install\` for full options.`,
135
+
136
+ 'sync-all': `\
137
+ duk sync-all <dir> — Batch update the harness in many projects
138
+
139
+ Iterates over every folder under <dir> that contains a .claude/
140
+ directory and runs duk install. Default is dry-run; pass --apply to
141
+ execute.
142
+
143
+ Usage:
144
+ duk sync-all <dir> [options]
145
+
146
+ Options:
147
+ --apply Execute the updates (default: dry-run preview).
148
+ --filter <expr> Filter projects (repeat for AND logic):
149
+ stack:<lang|framework> e.g. stack:java
150
+ type:<value> e.g. type:fullstack
151
+ age:<duration> e.g. age:30d, age:6m
152
+ harness-version:<semver> e.g. harness-version:<0.2
153
+ --exclude <name> Skip project by folder name (repeat for multiple).
154
+ --help, -h Show this help.
155
+
156
+ Examples:
157
+ duk sync-all C:\\development\\source\\projects
158
+ duk sync-all . --filter stack:java --apply
159
+ duk sync-all . --filter age:60d
160
+ duk sync-all . --exclude prod-critical --apply
161
+ duk sync-all . --filter harness-version:<0.2 --apply`,
162
+
163
+ dashboard: `\
164
+ duk dashboard — Start the local telemetry dashboard
165
+
166
+ Boots an Express server + Chart.js UI that visualises hook telemetry
167
+ from the local harness.
168
+
169
+ Usage:
170
+ duk dashboard [options]
171
+
172
+ Options:
173
+ --port <n> Port to bind to. Default: 4242.
174
+ --no-open Do not open the browser automatically.
175
+ --help, -h Show this help.
176
+
177
+ Examples:
178
+ duk dashboard
179
+ duk dashboard --port 4243
180
+ duk dashboard --no-open`,
181
+
182
+ help: `\
183
+ duk help [command] — Show help
184
+
185
+ Usage:
186
+ duk help Show full usage page.
187
+ duk help <command> Show detailed help for <command>.
188
+ duk <command> --help Same as \`duk help <command>\`.
189
+
190
+ Commands with detailed help:
191
+ new, install, update, sync-all, dashboard, help`,
192
+ };
193
+
194
+ /**
195
+ * Write the full general help to stdout.
196
+ */
197
+ function printGeneralHelp() {
198
+ process.stdout.write(GENERAL_HELP + '\n');
199
+ }
200
+
201
+ /**
202
+ * Write the short usage page to stdout (used when called with no args).
203
+ */
204
+ function printShortHelp() {
205
+ process.stdout.write(SHORT_HELP + '\n');
206
+ }
207
+
208
+ /**
209
+ * Write detailed help for a single command. Falls back to general help
210
+ * if the command is unknown.
211
+ * @param {string} command
212
+ */
213
+ function printCommandHelp(command) {
214
+ if (!command) {
215
+ printGeneralHelp();
216
+ return;
217
+ }
218
+ const normalised = command === 'update' ? 'update' : command;
219
+ const text = COMMAND_HELP[normalised];
220
+ if (!text) {
221
+ process.stderr.write(`Unknown command: ${command}\n\n`);
222
+ printGeneralHelp();
223
+ process.exitCode = 1;
224
+ return;
225
+ }
226
+ process.stdout.write(text + '\n');
227
+ }
228
+
229
+ module.exports = {
230
+ printGeneralHelp,
231
+ printShortHelp,
232
+ printCommandHelp,
233
+ };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CLAUDE.md `## Project Identity` helpers.
5
+ *
6
+ * - generateIdentitySection(dr) build a fresh Project Identity block
7
+ * - readIdentitySection(content) extract the existing block from CLAUDE.md
8
+ * - mergeClaudeMd(...) inject identity into the template body
9
+ * - emptyIdentitySection(name) placeholder identity for `duk new`
10
+ */
11
+
12
+ const IDENTITY_HEADING = '## Project Identity';
13
+
14
+ /**
15
+ * Build the Project Identity block pre-filled with detected values.
16
+ * @param {{ type: string, stackHints: string[] }} dr
17
+ * @returns {string}
18
+ */
19
+ function generateIdentitySection(dr) {
20
+ const stackLine = dr.stackHints && dr.stackHints.length > 0
21
+ ? dr.stackHints.join(', ')
22
+ : '<ex: Java 25 + Spring Boot 4 + Angular 21>';
23
+ return `## Project Identity
24
+
25
+ > **Modifique apenas esta seção ao adotar o plugin em um novo projeto.**
26
+ > Todas as demais seções são base do plugin e não devem ser alteradas diretamente
27
+ > — use \`update-template\` para receber atualizações.
28
+
29
+ - **Project name**: \`<project-name>\`
30
+ - **Project type**: \`${dr.type}\`
31
+ - **Primary stack**: \`${stackLine}\`
32
+ - **Database**: \`<ex: PostgreSQL 17 + Redis 7>\`
33
+ - **Domain**: \`<ex: e-commerce, fintech, healthcare>\`
34
+ - **Team size**: \`<ex: 3 backend, 2 frontend>\`
35
+ - **Additional rules**: _(deixe vazio se não houver)_`;
36
+ }
37
+
38
+ /**
39
+ * Placeholder identity used by `duk new <name>`. All fields are TODO so
40
+ * the stack-discovery skill can fill them in via conversation.
41
+ * @param {string} name
42
+ * @returns {string}
43
+ */
44
+ function emptyIdentitySection(name) {
45
+ return `## Project Identity
46
+
47
+ > **Modifique apenas esta seção ao adotar o plugin em um novo projeto.**
48
+ > Todas as demais seções são base do plugin e não devem ser alteradas diretamente
49
+ > — use \`update-template\` para receber atualizações.
50
+
51
+ - **Project name**: \`${name}\`
52
+ - **Project type**: \`<TODO: backend | frontend | fullstack | mobile | library | cli | data-pipeline>\`
53
+ - **Primary stack**: \`<TODO: declare via stack-discovery skill>\`
54
+ - **Database**: \`<TODO>\`
55
+ - **Domain**: \`<TODO>\`
56
+ - **Team size**: \`<TODO>\`
57
+ - **Additional rules**: _(deixe vazio se não houver)_`;
58
+ }
59
+
60
+ /**
61
+ * Extract the existing ## Project Identity section from a CLAUDE.md string.
62
+ * Returns the full section text (from the heading to before the next ## heading),
63
+ * or null if not found.
64
+ * @param {string} content
65
+ * @returns {string|null}
66
+ */
67
+ function readIdentitySection(content) {
68
+ const startIdx = content.indexOf(IDENTITY_HEADING);
69
+ if (startIdx === -1) return null;
70
+
71
+ const afterHeading = content.indexOf('\n## ', startIdx + IDENTITY_HEADING.length);
72
+ if (afterHeading === -1) {
73
+ return content.slice(startIdx).trimEnd();
74
+ }
75
+ return content.slice(startIdx, afterHeading).trimEnd();
76
+ }
77
+
78
+ /**
79
+ * Merge: replace the template's ## Project Identity placeholder with the
80
+ * preserved or generated identity section.
81
+ * @param {string} existing - current file content (may be empty, kept for symmetry)
82
+ * @param {string} template - template content from package
83
+ * @param {string} identity - identity section text to inject
84
+ * @returns {string}
85
+ */
86
+ function mergeClaudeMd(existing, template, identity) {
87
+ const templateStartIdx = template.indexOf(IDENTITY_HEADING);
88
+ if (templateStartIdx === -1) {
89
+ return identity + '\n\n' + template;
90
+ }
91
+
92
+ const afterHeading = template.indexOf('\n## ', templateStartIdx + IDENTITY_HEADING.length);
93
+ let rest;
94
+ if (afterHeading === -1) {
95
+ rest = '';
96
+ } else {
97
+ rest = template.slice(afterHeading);
98
+ }
99
+ return identity + rest;
100
+ }
101
+
102
+ module.exports = {
103
+ IDENTITY_HEADING,
104
+ generateIdentitySection,
105
+ emptyIdentitySection,
106
+ readIdentitySection,
107
+ mergeClaudeMd,
108
+ };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Provision `.claude/local/` with a README explaining the override contract
8
+ * (per ADR-032). Idempotent — never overwrites an existing README.
9
+ */
10
+
11
+ const LOCAL_README = `# .claude/local/ — project-only overrides
12
+
13
+ This directory is **never touched** by \`duk install\`. Anything you put
14
+ here survives every harness update.
15
+
16
+ Use it for customisations that apply ONLY to this project:
17
+
18
+ local/agents/ project-specific agent overrides
19
+ local/skills/ project-specific skills (non-canonical)
20
+ local/stacks/ proprietary or internal stack packs
21
+
22
+ Loader priority (per ADR-032 §3):
23
+ .claude/local/<path> wins over
24
+ .claude/<path>
25
+
26
+ When to use \`.claude/local/\`:
27
+ - Stack proprietária da empresa (ex: \`local/stacks/internal/cobol.md\`)
28
+ - Regra de projeto que NÃO se aplica a outros
29
+ - Skill experimental que pode virar canônica via PR no harness
30
+
31
+ When NOT to use:
32
+ - "Prefiro outro estilo" → Honcho (\`lembra que prefiro X\`)
33
+ - Algo que beneficia todos → PR no harness repo
34
+
35
+ Drift detection: files under \`local/\` are excluded from \`.MANIFEST\`,
36
+ so editing them never triggers a drift abort during \`duk install\`.
37
+ `;
38
+
39
+ /**
40
+ * Ensure `.claude/local/` exists with a README. Subdirs (agents/, skills/,
41
+ * stacks/) are NOT pre-created — they are lazy, born when the dev adds an
42
+ * override.
43
+ *
44
+ * @param {string} claudeDir absolute path to .claude/
45
+ * @param {boolean} dryRun
46
+ */
47
+ function ensureLocalDir(claudeDir, dryRun) {
48
+ const localDir = path.join(claudeDir, 'local');
49
+ const readmePath = path.join(localDir, 'README.md');
50
+
51
+ if (dryRun) {
52
+ if (!fs.existsSync(localDir)) {
53
+ process.stdout.write(' [dry-run] would create .claude/local/ with README.md\n');
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (!fs.existsSync(localDir)) {
59
+ fs.mkdirSync(localDir, { recursive: true });
60
+ }
61
+ if (!fs.existsSync(readmePath)) {
62
+ fs.writeFileSync(readmePath, LOCAL_README, 'utf8');
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ ensureLocalDir,
68
+ LOCAL_README,
69
+ };
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * `.claude/.MANIFEST` — hash-based drift detection per ADR-032.
9
+ *
10
+ * generateManifest(claudeDir) walk .claude/ and return Map<relPath, sha256>
11
+ * writeManifest(claudeDir, harnessVersion)
12
+ * loadManifest(claudeDir) parse .claude/.MANIFEST or return null
13
+ * detectDrift(claudeDir) compare current files vs stored manifest
14
+ *
15
+ * Paths excluded from the manifest (managed independently):
16
+ * - local/ (per-project overrides — never touched by install)
17
+ * - .MANIFEST (the manifest file itself)
18
+ * - HARNESS_VERSION
19
+ * - Anything under .claude.bak / .claude.backup-* (lives outside .claude/)
20
+ */
21
+
22
+ const MANIFEST_FILENAME = '.MANIFEST';
23
+ const HARNESS_VERSION_FILENAME = 'HARNESS_VERSION';
24
+ const LOCAL_DIR = 'local';
25
+
26
+ /**
27
+ * Walk dir recursively, returning relative POSIX-style paths of files,
28
+ * skipping any directory or filename that matches `shouldSkip(name, relPath)`.
29
+ * @param {string} root
30
+ * @param {(name: string, relPath: string) => boolean} shouldSkip
31
+ * @returns {string[]} sorted relative paths
32
+ */
33
+ function walkFiles(root, shouldSkip) {
34
+ const out = [];
35
+
36
+ function recur(absDir, relDir) {
37
+ let entries;
38
+ try {
39
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
40
+ } catch {
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
45
+ if (shouldSkip(entry.name, relPath)) continue;
46
+ const absPath = path.join(absDir, entry.name);
47
+ if (entry.isDirectory()) {
48
+ recur(absPath, relPath);
49
+ } else if (entry.isFile()) {
50
+ out.push(relPath);
51
+ }
52
+ }
53
+ }
54
+
55
+ recur(root, '');
56
+ out.sort();
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Return sha256 hex of a file's bytes.
62
+ * @param {string} absPath
63
+ * @returns {string}
64
+ */
65
+ function sha256OfFile(absPath) {
66
+ const buf = fs.readFileSync(absPath);
67
+ return crypto.createHash('sha256').update(buf).digest('hex');
68
+ }
69
+
70
+ /**
71
+ * Default skip rule for manifest walks.
72
+ * @param {string} name
73
+ * @param {string} relPath
74
+ * @returns {boolean}
75
+ */
76
+ function defaultSkip(name, relPath) {
77
+ // Skip top-level local/, MANIFEST, HARNESS_VERSION
78
+ if (relPath === LOCAL_DIR) return true;
79
+ if (relPath.startsWith(`${LOCAL_DIR}/`)) return true;
80
+ if (relPath === MANIFEST_FILENAME) return true;
81
+ if (relPath === HARNESS_VERSION_FILENAME) return true;
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Build a Map<relPath, sha256> for every file under .claude/ except the
87
+ * exclusions listed above.
88
+ * @param {string} claudeDir absolute path to the .claude/ directory
89
+ * @returns {Map<string, string>}
90
+ */
91
+ function generateManifest(claudeDir) {
92
+ const result = new Map();
93
+ if (!fs.existsSync(claudeDir)) return result;
94
+ const files = walkFiles(claudeDir, defaultSkip);
95
+ for (const rel of files) {
96
+ const abs = path.join(claudeDir, rel);
97
+ result.set(rel, sha256OfFile(abs));
98
+ }
99
+ return result;
100
+ }
101
+
102
+ /**
103
+ * Render a manifest Map to the on-disk format and write it to
104
+ * `<claudeDir>/.MANIFEST`.
105
+ * @param {string} claudeDir
106
+ * @param {string} harnessVersion
107
+ * @param {Map<string, string>} [manifest] - pre-computed; if omitted, regenerates
108
+ */
109
+ function writeManifest(claudeDir, harnessVersion, manifest) {
110
+ const map = manifest || generateManifest(claudeDir);
111
+ const ts = new Date().toISOString();
112
+ const header = [
113
+ '# .claude/.MANIFEST',
114
+ `# Auto-generated by duk install at ${ts}.`,
115
+ `# Harness version: ${harnessVersion}`,
116
+ '# Format: <relative-path> <sha256>',
117
+ '# Do not edit by hand.',
118
+ ];
119
+ const lines = [...map.entries()]
120
+ .sort((a, b) => a[0].localeCompare(b[0]))
121
+ .map(([rel, sha]) => `${rel} ${sha}`);
122
+ const body = header.concat(lines).join('\n') + '\n';
123
+ const target = path.join(claudeDir, MANIFEST_FILENAME);
124
+ fs.writeFileSync(target, body, 'utf8');
125
+ }
126
+
127
+ /**
128
+ * Read and parse `.claude/.MANIFEST`. Returns null if missing or unreadable.
129
+ * @param {string} claudeDir
130
+ * @returns {Map<string, string> | null}
131
+ */
132
+ function loadManifest(claudeDir) {
133
+ const target = path.join(claudeDir, MANIFEST_FILENAME);
134
+ if (!fs.existsSync(target)) return null;
135
+
136
+ let content;
137
+ try {
138
+ content = fs.readFileSync(target, 'utf8');
139
+ } catch {
140
+ return null;
141
+ }
142
+
143
+ const map = new Map();
144
+ for (const line of content.split(/\r?\n/)) {
145
+ if (!line || line.startsWith('#')) continue;
146
+ // last whitespace-separated chunk is the sha; rest is path
147
+ const idx = line.lastIndexOf(' ');
148
+ if (idx === -1) continue;
149
+ const rel = line.slice(0, idx).trim();
150
+ const sha = line.slice(idx + 1).trim();
151
+ if (rel && /^[a-f0-9]{64}$/i.test(sha)) {
152
+ map.set(rel, sha);
153
+ }
154
+ }
155
+ return map;
156
+ }
157
+
158
+ /**
159
+ * Compare current files in .claude/ against the stored manifest. Returns
160
+ * { hasManifest, drifted, added, removed }
161
+ * where each list contains relative paths.
162
+ *
163
+ * - hasManifest=false → no baseline (first install ever) → not blocking.
164
+ * - drifted → file present in both but sha differs.
165
+ * - added → file present on disk but not in manifest.
166
+ * - removed → file in manifest but missing on disk.
167
+ *
168
+ * @param {string} claudeDir
169
+ * @returns {{ hasManifest: boolean, drifted: string[], added: string[], removed: string[] }}
170
+ */
171
+ function detectDrift(claudeDir) {
172
+ const stored = loadManifest(claudeDir);
173
+ if (stored === null) {
174
+ return { hasManifest: false, drifted: [], added: [], removed: [] };
175
+ }
176
+ const current = generateManifest(claudeDir);
177
+
178
+ const drifted = [];
179
+ const added = [];
180
+ const removed = [];
181
+
182
+ for (const [rel, sha] of current.entries()) {
183
+ if (!stored.has(rel)) {
184
+ added.push(rel);
185
+ } else if (stored.get(rel) !== sha) {
186
+ drifted.push(rel);
187
+ }
188
+ }
189
+ for (const rel of stored.keys()) {
190
+ if (!current.has(rel)) {
191
+ removed.push(rel);
192
+ }
193
+ }
194
+
195
+ drifted.sort();
196
+ added.sort();
197
+ removed.sort();
198
+ return { hasManifest: true, drifted, added, removed };
199
+ }
200
+
201
+ /**
202
+ * Write the plain-text HARNESS_VERSION file (single line, semver).
203
+ * @param {string} claudeDir
204
+ * @param {string} version
205
+ */
206
+ function writeHarnessVersion(claudeDir, version) {
207
+ const target = path.join(claudeDir, HARNESS_VERSION_FILENAME);
208
+ fs.writeFileSync(target, version + '\n', 'utf8');
209
+ }
210
+
211
+ /**
212
+ * Read HARNESS_VERSION, returning null if missing.
213
+ * @param {string} claudeDir
214
+ * @returns {string | null}
215
+ */
216
+ function readHarnessVersion(claudeDir) {
217
+ const target = path.join(claudeDir, HARNESS_VERSION_FILENAME);
218
+ if (!fs.existsSync(target)) return null;
219
+ try {
220
+ return fs.readFileSync(target, 'utf8').trim();
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ module.exports = {
227
+ MANIFEST_FILENAME,
228
+ HARNESS_VERSION_FILENAME,
229
+ LOCAL_DIR,
230
+ generateManifest,
231
+ writeManifest,
232
+ loadManifest,
233
+ detectDrift,
234
+ writeHarnessVersion,
235
+ readHarnessVersion,
236
+ };