devmethod-ai 0.1.0-rc.2 → 0.2.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 (228) hide show
  1. package/.agents/skills/decision-architecture/SKILL.md +15 -13
  2. package/.agents/skills/decision-architecture/assets/ADR.md +18 -17
  3. package/.agents/skills/decision-architecture/references/api-contracts.md +21 -16
  4. package/.agents/skills/decision-architecture/references/backend-boundaries.md +21 -18
  5. package/.agents/skills/decision-architecture/references/product-decisions.md +14 -17
  6. package/.agents/skills/design-to-code/SKILL.md +18 -11
  7. package/.agents/skills/design-to-code/assets/UI_ACCEPTANCE.md +13 -12
  8. package/.agents/skills/design-to-code/references/ux-contract.md +20 -15
  9. package/.agents/skills/design-to-code/references/visual-creation.md +37 -0
  10. package/.agents/skills/project-foundation/SKILL.md +39 -30
  11. package/.agents/skills/project-foundation/assets/AGENTS.foundation.md +16 -15
  12. package/.agents/skills/project-foundation/assets/PROJECT_PROFILE.md +24 -23
  13. package/.agents/skills/project-foundation/assets/START_HERE.md +17 -14
  14. package/.agents/skills/project-foundation/references/mission-context.md +30 -0
  15. package/.agents/skills/project-foundation/references/operating-commands.md +49 -34
  16. package/.agents/skills/project-foundation/references/profiles/README.md +18 -0
  17. package/.agents/skills/project-foundation/references/profiles/ci.md +11 -0
  18. package/.agents/skills/project-foundation/references/profiles/cloud-delivery.md +13 -0
  19. package/.agents/skills/project-foundation/references/profiles/messaging.md +11 -0
  20. package/.agents/skills/project-foundation/references/profiles/mongodb.md +11 -0
  21. package/.agents/skills/project-foundation/references/profiles/node-nest.md +11 -0
  22. package/.agents/skills/project-foundation/references/profiles/postgres-drizzle.md +11 -0
  23. package/.agents/skills/project-foundation/references/profiles/react-next.md +11 -0
  24. package/.agents/skills/project-foundation/references/profiles/typescript.md +11 -0
  25. package/.agents/skills/project-foundation/references/work-sizing.md +31 -0
  26. package/.agents/skills/react-feature-engineering/SKILL.md +26 -22
  27. package/.agents/skills/react-feature-engineering/references/review-and-sources.md +41 -37
  28. package/.agents/skills/reliable-ai-integration/SKILL.md +13 -10
  29. package/.agents/skills/reliable-ai-integration/assets/AI_EVALUATION.md +19 -18
  30. package/.agents/skills/reliable-ai-integration/references/evidence-and-media.md +22 -18
  31. package/.agents/skills/reliable-ai-integration/references/jobs-and-costs.md +20 -15
  32. package/.agents/skills/scoped-delivery/SKILL.md +25 -18
  33. package/.agents/skills/scoped-delivery/assets/CHECKPOINT.md +22 -11
  34. package/.agents/skills/scoped-delivery/assets/MISSION.md +35 -0
  35. package/.agents/skills/scoped-delivery/assets/SLICE.md +15 -14
  36. package/.agents/skills/scoped-delivery/assets/VERIFICATION.md +19 -0
  37. package/.agents/skills/scoped-delivery/references/verification-and-cost.md +21 -18
  38. package/COMPATIBILITY.md +3 -3
  39. package/CONTRIBUTING.md +13 -0
  40. package/README.md +94 -9
  41. package/START_HERE.md +24 -0
  42. package/VALIDATION.md +37 -0
  43. package/dist/checkpoint.js +171 -0
  44. package/dist/cli.js +118 -17
  45. package/dist/doctor.js +113 -0
  46. package/dist/filesystem.js +32 -0
  47. package/dist/init.js +38 -34
  48. package/dist/mission.js +85 -0
  49. package/dist/planner.js +65 -0
  50. package/dist/records.js +76 -0
  51. package/dist/update.js +57 -0
  52. package/docs/ADR-001-distribution.md +13 -0
  53. package/docs/ADR-002-read-only-diagnostics.md +25 -0
  54. package/docs/ADR-003-installation-provenance.md +15 -0
  55. package/docs/ADR-004-checkpoint-resumption.md +23 -0
  56. package/docs/ADR-005-mission-context.md +11 -0
  57. package/docs/ADR-006-opt-in-native-pilot.md +15 -0
  58. package/docs/BMAD-COMPARISON.md +34 -0
  59. package/docs/CODEX-ADAPTER-VALIDATION.md +20 -0
  60. package/docs/COMPARISON-ITERATION.md +25 -0
  61. package/docs/DOCTOR.md +23 -0
  62. package/docs/EVALUATION.md +39 -0
  63. package/docs/EVOLUTION.md +34 -0
  64. package/docs/GREENFIELD-VALIDATION.md +36 -0
  65. package/docs/MILESTONES.md +39 -0
  66. package/docs/MISSIONS.md +47 -0
  67. package/docs/NATIVE-PILOT-RESULTS.md +22 -0
  68. package/docs/NATIVE-PILOT.md +17 -0
  69. package/docs/ORCHESTRATION.md +11 -0
  70. package/docs/RC2-VALIDATION.md +50 -0
  71. package/docs/RELEASE-0.1.0.md +60 -0
  72. package/docs/RELEASE-0.2.0.md +37 -0
  73. package/docs/RELEASE-CHECKLIST.md +13 -0
  74. package/docs/RELEASE-NOTES-0.1.0-rc.2.md +32 -0
  75. package/docs/RELEASE-NOTES-0.1.0.md +11 -0
  76. package/docs/RESUMPTION.md +87 -0
  77. package/docs/ROADMAP.md +39 -0
  78. package/docs/STACK-PROFILES.md +9 -0
  79. package/docs/TROUBLESHOOTING.md +16 -0
  80. package/docs/UPDATES.md +30 -0
  81. package/docs/VISUAL-CREATION-PROPOSAL.md +21 -0
  82. package/docs/VISUAL-WORKFLOW.md +75 -0
  83. package/docs/images/devmethod-delivery.svg +10 -0
  84. package/docs/images/devmethod-flow.svg +13 -0
  85. package/docs/media/from-zero/README.md +17 -0
  86. package/docs/media/from-zero/devmethod-demo.fr.srt +59 -0
  87. package/docs/media/from-zero/scenes.json +50 -0
  88. package/docs/media/full-chain-4k/README.md +37 -0
  89. package/docs/media/full-chain-4k/devmethod-chaine-complete.fr.srt +435 -0
  90. package/docs/media/full-chain-4k/scenes.json +173 -0
  91. package/docs/media/full-chain-4k/transcripts.fr.md +315 -0
  92. package/docs/media/visual-chain/README.md +44 -0
  93. package/docs/media/visual-chain/assets/add-book-v1.png +0 -0
  94. package/docs/media/visual-chain/assets/completed-v1.png +0 -0
  95. package/docs/media/visual-chain/devmethod-du-besoin-au-produit.fr.srt +147 -0
  96. package/docs/media/visual-chain/execution.fr.md +30 -0
  97. package/docs/media/visual-chain/image-prompts.json +9 -0
  98. package/docs/media/visual-chain/reference-hashes.json +5 -0
  99. package/docs/media/visual-chain/scenes.json +232 -0
  100. package/docs/media/visual-chain/video-preview.jpg +0 -0
  101. package/docs/missions/visual-workflow.md +46 -0
  102. package/evaluation/COMPARISONS.md +33 -0
  103. package/evaluation/README.md +35 -0
  104. package/evaluation/candidate-setup.json +33 -0
  105. package/evaluation/comparison-v2-readiness.json +18 -0
  106. package/evaluation/comparison-v2-results.json +58 -0
  107. package/evaluation/fixtures/dependency/CONTRIBUTING.md +3 -0
  108. package/evaluation/fixtures/dependency/TICKET.md +3 -0
  109. package/evaluation/fixtures/dependency/acceptance.test.mjs +4 -0
  110. package/evaluation/fixtures/dependency/api.mjs +1 -0
  111. package/evaluation/fixtures/failing-verification/CONTRIBUTING.md +3 -0
  112. package/evaluation/fixtures/failing-verification/acceptance.test.mjs +4 -0
  113. package/evaluation/fixtures/failing-verification/total.mjs +1 -0
  114. package/evaluation/fixtures/stale-handoff/CHECKPOINT.md +3 -0
  115. package/evaluation/fixtures/stale-handoff/CONTRACT.md +3 -0
  116. package/evaluation/fixtures/stale-handoff/CONTRIBUTING.md +3 -0
  117. package/evaluation/fixtures/stale-handoff/acceptance.test.mjs +4 -0
  118. package/evaluation/fixtures/stale-handoff/label.mjs +1 -0
  119. package/evaluation/fixtures/update-customization/README.md +13 -0
  120. package/evaluation/fixtures.json +85 -0
  121. package/evaluation/greenfield/acceptance.test.mjs +57 -0
  122. package/evaluation/greenfield/security.test.mjs +28 -0
  123. package/evaluation/native-pilot-results.json +155 -0
  124. package/examples/README.md +36 -0
  125. package/examples/bugfix/acceptance.test.mjs +17 -0
  126. package/examples/bugfix/page-size.mjs +4 -0
  127. package/examples/clair-from-zero/AGENT-EVALUATION.md +15 -0
  128. package/examples/clair-from-zero/MISSION.md +41 -0
  129. package/examples/clair-from-zero/README.md +24 -0
  130. package/examples/clair-from-zero/app/app.mjs +69 -0
  131. package/examples/clair-from-zero/app/domain.mjs +30 -0
  132. package/examples/clair-from-zero/app/index.html +20 -0
  133. package/examples/clair-from-zero/app/storage.mjs +4 -0
  134. package/examples/clair-from-zero/app/styles.css +128 -0
  135. package/examples/clair-from-zero/browser-check.cjs +2 -0
  136. package/examples/clair-from-zero/tests/domain.test.mjs +37 -0
  137. package/examples/fullstack/CONTRACT.md +23 -0
  138. package/examples/fullstack/DEPENDENCIES.md +18 -0
  139. package/examples/fullstack/README.md +100 -0
  140. package/examples/fullstack/api/application/tasks.ts +13 -0
  141. package/examples/fullstack/api/domain/task.ts +12 -0
  142. package/examples/fullstack/api/http.ts +26 -0
  143. package/examples/fullstack/api/infrastructure/migrate.ts +15 -0
  144. package/examples/fullstack/api/infrastructure/postgres.ts +18 -0
  145. package/examples/fullstack/api/infrastructure/schema.ts +6 -0
  146. package/examples/fullstack/api/main.ts +17 -0
  147. package/examples/fullstack/api/tsconfig.json +17 -0
  148. package/examples/fullstack/compose.yaml +14 -0
  149. package/examples/fullstack/migrations/0000_tasks.sql +5 -0
  150. package/examples/fullstack/migrations/meta/_journal.json +13 -0
  151. package/examples/fullstack/mission.json +34 -0
  152. package/examples/fullstack/package-lock.json +2661 -0
  153. package/examples/fullstack/package.json +45 -0
  154. package/examples/fullstack/tests/e2e.test.cjs +54 -0
  155. package/examples/fullstack/tests/http.test.cjs +33 -0
  156. package/examples/fullstack/tests/postgres.test.cjs +29 -0
  157. package/examples/fullstack/tests/unit.test.cjs +20 -0
  158. package/examples/fullstack/tests/web-model.test.cjs +23 -0
  159. package/examples/fullstack/web/app/layout.tsx +4 -0
  160. package/examples/fullstack/web/app/page.tsx +11 -0
  161. package/examples/fullstack/web/features/tasks/components/task-list.tsx +11 -0
  162. package/examples/fullstack/web/features/tasks/hooks/use-task-filter.ts +7 -0
  163. package/examples/fullstack/web/features/tasks/model/tasks.ts +12 -0
  164. package/examples/fullstack/web/features/tasks/server/list-tasks.ts +8 -0
  165. package/examples/fullstack/web/next.config.mjs +2 -0
  166. package/examples/fullstack/web/tsconfig.json +36 -0
  167. package/examples/mission/CONTRACT.md +3 -0
  168. package/examples/mission/mission.json +18 -0
  169. package/examples/mission/plan.json +11 -0
  170. package/examples/pocket-tasks/BRIEF.md +21 -0
  171. package/examples/pocket-tasks/PROJECT_PROFILE.md +19 -0
  172. package/examples/pocket-tasks/README.md +43 -0
  173. package/examples/pocket-tasks/docs/ADR-001.md +7 -0
  174. package/examples/pocket-tasks/docs/HANDOFF.md +16 -0
  175. package/examples/pocket-tasks/docs/MISSION.md +15 -0
  176. package/examples/pocket-tasks/docs/VERIFICATION.md +58 -0
  177. package/examples/pocket-tasks/package.json +1 -0
  178. package/examples/pocket-tasks/public/app.js +84 -0
  179. package/examples/pocket-tasks/public/index.html +12 -0
  180. package/examples/pocket-tasks/public/styles.css +1 -0
  181. package/examples/pocket-tasks/server.mjs +79 -0
  182. package/examples/pocket-tasks/src/domain.mjs +31 -0
  183. package/examples/pocket-tasks/src/store.mjs +49 -0
  184. package/examples/pocket-tasks/tests/app.test.mjs +132 -0
  185. package/examples/pocket-tasks/tests/ui-errors.test.mjs +49 -0
  186. package/examples/visual-pilot/README.md +11 -0
  187. package/examples/visual-pilot/app/app.js +10 -0
  188. package/examples/visual-pilot/app/index.html +1 -0
  189. package/examples/visual-pilot/app/reference.png +0 -0
  190. package/examples/visual-pilot/app/style.css +3 -0
  191. package/examples/visual-pilot/browser-check.cjs +3 -0
  192. package/examples/visual-pilot/desktop-actual.png +0 -0
  193. package/examples/visual-pilot/directions-prompt.txt +1 -0
  194. package/examples/visual-pilot/directions-v1.png +0 -0
  195. package/examples/visual-pilot/editorial-mockup-prompt.txt +1 -0
  196. package/examples/visual-pilot/editorial-mockup-v1.png +0 -0
  197. package/examples/visual-pilot/mismatch-probe.png +0 -0
  198. package/examples/visual-pilot/mobile-actual.png +0 -0
  199. package/examples/visual-pilot/quick-filter/AGENT-RESULT.md +25 -0
  200. package/examples/visual-pilot/quick-filter/baseline.log +42 -0
  201. package/examples/visual-pilot/quick-filter/filter.mjs +3 -0
  202. package/examples/visual-pilot/quick-filter/filter.test.mjs +9 -0
  203. package/package.json +15 -3
  204. package/scripts/check-codex-git.mjs +20 -0
  205. package/scripts/check-docs.mjs +14 -0
  206. package/scripts/check-native-sandbox.mjs +11 -0
  207. package/scripts/comparison-readiness.mjs +26 -0
  208. package/scripts/comparison.mjs +42 -0
  209. package/scripts/evaluation.mjs +64 -0
  210. package/scripts/fullstack-mission.mjs +30 -0
  211. package/scripts/hosts/checks.mjs +13 -0
  212. package/scripts/hosts/codex-meter.mjs +17 -0
  213. package/scripts/hosts/codex-rpc.mjs +44 -0
  214. package/scripts/hosts/codex-task.mjs +29 -0
  215. package/scripts/hosts/codex.mjs +28 -0
  216. package/scripts/media/demo-actions.cjs +16 -0
  217. package/scripts/media/encode-demo.py +22 -0
  218. package/scripts/media/record-demo.cjs +30 -0
  219. package/scripts/media/visual-short/check.cjs +1 -0
  220. package/scripts/media/visual-short/encode.py +16 -0
  221. package/scripts/media/visual-short/record.cjs +4 -0
  222. package/scripts/media/visual-short/story.py +34 -0
  223. package/scripts/native-host.mjs +55 -0
  224. package/scripts/package-smoke.mjs +64 -0
  225. package/scripts/probe-codex-children.mjs +41 -0
  226. package/scripts/run-comparison-v2.mjs +45 -0
  227. package/scripts/run-native-pilot.mjs +76 -0
  228. package/scripts/summarize-native-pilot.mjs +7 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,113 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { tools, modules, templates } from './init.js';
5
+ import { parseJson, checkPath, stat } from './filesystem.js';
6
+ function object(value) {
7
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+ export function validateManifest(value) {
10
+ if (!object(value) || value.format !== 2 || value.kit !== 'devmethod' ||
11
+ typeof value.tool !== 'string' || !Object.hasOwn(tools, value.tool) ||
12
+ !Array.isArray(value.skills) || !value.skills.includes('project-foundation') ||
13
+ new Set(value.skills).size !== value.skills.length ||
14
+ !value.skills.every(name => typeof name === 'string' && modules.includes(name)) ||
15
+ !object(value.files))
16
+ throw new Error('Expected a DevMethod format 2 manifest with a supported host and unique modules including project-foundation.');
17
+ const root = tools[value.tool];
18
+ const required = [...templates, 'DEVMETHOD-LICENSE', ...value.skills.map(name => `${root}/${name}/SKILL.md`)];
19
+ for (const name of required) {
20
+ if (!Object.hasOwn(value.files, name))
21
+ throw new Error(`Manifest is missing required entry: ${name}`);
22
+ }
23
+ // Validate every entry before any payload reads. A manifest never grants filesystem access.
24
+ for (const [name, hash] of Object.entries(value.files)) {
25
+ const parts = name.split('/');
26
+ const safe = parts.every(part => /^[a-zA-Z0-9._-]+$/.test(part) && part !== '.' && part !== '..');
27
+ const rootFile = templates.includes(name) || name === 'DEVMETHOD-LICENSE';
28
+ const skillFile = parts.slice(0, 2).join('/') === root && value.skills.includes(parts[2]) &&
29
+ ((parts.length === 4 && parts[3] === 'SKILL.md') ||
30
+ (parts.length >= 5 && ['assets', 'references'].includes(parts[3] ?? '') && name.endsWith('.md')));
31
+ if (!safe || (!rootFile && !skillFile))
32
+ throw new Error(`Manifest contains an unsupported path: ${name}`);
33
+ if (typeof hash !== 'string' || !/^[a-f0-9]{64}$/i.test(hash))
34
+ throw new Error(`Invalid SHA-256 for: ${name}`);
35
+ }
36
+ if (value.provenance !== undefined) {
37
+ const p = value.provenance;
38
+ if (!object(p) || p.packageName !== 'devmethod-ai' || typeof p.packageVersion !== 'string' ||
39
+ !/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.+-]+)?$/.test(p.packageVersion) ||
40
+ typeof p.payloadSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(p.payloadSha256))
41
+ throw new Error('Invalid installation provenance.');
42
+ }
43
+ return value;
44
+ }
45
+ /** Inspect the recorded installation baseline without writes, commands or network calls. */
46
+ export function diagnose(destination) {
47
+ const report = { format: 1, destination: path.resolve(destination), status: 'ok', checked: 0, unchanged: 0, findings: [] };
48
+ const add = (severity, code, message, file) => {
49
+ report.findings.push({ severity, code, ...(file === undefined ? {} : { path: file }), message });
50
+ if (severity === 'error' || report.status === 'ok')
51
+ report.status = severity;
52
+ };
53
+ let manifest;
54
+ try {
55
+ const file = path.join(report.destination, 'kit-manifest.json');
56
+ checkPath(file);
57
+ const info = stat(file);
58
+ if (!info) {
59
+ add('error', 'manifest-missing', 'No installation manifest. Check --dest or install into a fresh staging directory.', 'kit-manifest.json');
60
+ return report;
61
+ }
62
+ if (!info.isFile() || info.size > 1024 * 1024)
63
+ throw new Error('Manifest must be a regular file no larger than 1 MiB.');
64
+ manifest = validateManifest(parseJson(fs.readFileSync(file, 'utf8')));
65
+ }
66
+ catch (error) {
67
+ add('error', 'manifest-invalid', error instanceof Error ? error.message : String(error), 'kit-manifest.json');
68
+ return report;
69
+ }
70
+ report.tool = manifest.tool;
71
+ report.skills = manifest.skills;
72
+ for (const [relative, expected] of Object.entries(manifest.files)) {
73
+ report.checked++;
74
+ try {
75
+ const file = path.join(report.destination, relative);
76
+ checkPath(file);
77
+ const info = stat(file);
78
+ if (!info) {
79
+ add('error', 'file-missing', 'Restore from a reviewed staging installation.', relative);
80
+ continue;
81
+ }
82
+ if (!info.isFile()) {
83
+ add('error', 'file-type', 'Expected a regular file.', relative);
84
+ continue;
85
+ }
86
+ const actual = createHash('sha256').update(fs.readFileSync(file)).digest('hex');
87
+ if (actual === expected.toLowerCase())
88
+ report.unchanged++;
89
+ else
90
+ add('warning', 'file-modified', 'Differs from the initial installation. Review intentional customization; nothing was overwritten.', relative);
91
+ }
92
+ catch (error) {
93
+ add('error', 'file-unreadable', error instanceof Error ? error.message : String(error), relative);
94
+ }
95
+ }
96
+ for (const name of manifest.skills) {
97
+ for (const root of Object.values(tools)) {
98
+ if (root === tools[manifest.tool])
99
+ continue;
100
+ const relative = `${root}/${name}`;
101
+ try {
102
+ const file = path.join(report.destination, relative);
103
+ checkPath(file);
104
+ if (stat(file))
105
+ add('error', 'duplicate-host', 'Another host contains this module. Keep one reviewed copy per project.', relative);
106
+ }
107
+ catch (error) {
108
+ add('error', 'host-unreadable', error instanceof Error ? error.message : String(error), relative);
109
+ }
110
+ }
111
+ }
112
+ return report;
113
+ }
@@ -0,0 +1,32 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function stat(file) {
4
+ try {
5
+ return fs.lstatSync(file);
6
+ }
7
+ catch (error) {
8
+ if (error.code === 'ENOENT')
9
+ return undefined;
10
+ throw error;
11
+ }
12
+ }
13
+ export function checkPath(file) {
14
+ for (let current = file;; current = path.dirname(current)) {
15
+ const info = stat(current);
16
+ if (info?.isSymbolicLink())
17
+ throw new Error(`Symbolic links are not accepted: ${current}`);
18
+ if (current !== file && info && !info.isDirectory())
19
+ throw new Error(`Not a directory: ${current}`);
20
+ if (path.dirname(current) === current)
21
+ break;
22
+ }
23
+ }
24
+ /** Parse errors must never echo input bytes into logs or JSON reports. */
25
+ export function parseJson(text) {
26
+ try {
27
+ return JSON.parse(text);
28
+ }
29
+ catch {
30
+ throw new Error('Invalid JSON record; source text omitted.');
31
+ }
32
+ }
package/dist/init.js CHANGED
@@ -2,31 +2,11 @@ import * as fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { parseJson, checkPath, stat } from './filesystem.js';
5
6
  export const tools = { codex: '.agents/skills', claude: '.claude/skills', cursor: '.cursor/skills' };
6
7
  export const modules = ['project-foundation', 'decision-architecture', 'design-to-code', 'react-feature-engineering', 'reliable-ai-integration', 'scoped-delivery'];
7
- const templates = ['PROJECT_PROFILE.md', 'AGENTS.foundation.md', 'START_HERE.md', 'ENGINEERING_POLICY.template.md'];
8
+ export const templates = ['PROJECT_PROFILE.md', 'AGENTS.foundation.md', 'START_HERE.md', 'ENGINEERING_POLICY.template.md'];
8
9
  const packageRoot = fileURLToPath(new URL('../', import.meta.url));
9
- function stat(file) {
10
- try {
11
- return fs.lstatSync(file);
12
- }
13
- catch (error) {
14
- if (error.code === 'ENOENT')
15
- return undefined;
16
- throw error;
17
- }
18
- }
19
- function checkPath(file) {
20
- for (let current = file;; current = path.dirname(current)) {
21
- const info = stat(current);
22
- if (info?.isSymbolicLink())
23
- throw new Error(`Symbolic links are not accepted: ${current}`);
24
- if (current !== file && info && !info.isDirectory())
25
- throw new Error(`Not a directory: ${current}`);
26
- if (path.dirname(current) === current)
27
- break;
28
- }
29
- }
30
10
  function walk(directory, prefix = '') {
31
11
  return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap(entry => {
32
12
  const relative = prefix + entry.name;
@@ -55,31 +35,32 @@ export function initialize(options) {
55
35
  throw new Error('Install outside the distribution directory');
56
36
  if (stat(destination) && !stat(destination)?.isDirectory())
57
37
  throw new Error('Destination must be a directory');
58
- const files = new Map();
59
38
  for (const name of selected) {
60
39
  for (const otherRoot of Object.values(tools)) {
61
40
  if (otherRoot !== tools[options.tool] && stat(path.join(destination, otherRoot, name)))
62
41
  throw new Error(`Duplicate skill in another host directory: ${otherRoot}/${name}`);
63
42
  }
64
- const source = path.join(packageRoot, '.agents/skills', name);
65
- checkPath(source);
66
- for (const relative of walk(source)) {
67
- if (!/^(SKILL\.md|assets\/.*\.md|references\/.*\.md)$/.test(relative))
68
- throw new Error(`Unexpected payload file: ${name}/${relative}`);
69
- files.set(`${tools[options.tool]}/${name}/${relative}`, profile(fs.readFileSync(path.join(source, relative)), options.tool));
70
- }
71
43
  }
72
- for (const template of templates)
73
- files.set(template, profile(fs.readFileSync(path.join(packageRoot, '.agents/skills/project-foundation/assets', template)), options.tool));
74
- files.set('DEVMETHOD-LICENSE', fs.readFileSync(path.join(packageRoot, 'LICENSE')));
44
+ const files = bundledFiles(options.tool, selected);
75
45
  const hashes = Object.fromEntries([...files].map(([name, data]) => [name, createHash('sha256').update(data).digest('hex')]));
76
- files.set('kit-manifest.json', Buffer.from(JSON.stringify({ format: 2, kit: 'devmethod', tool: options.tool, skills: selected, files: hashes }, null, 2) + '\n'));
46
+ const manifest = { format: 2, kit: 'devmethod', tool: options.tool, skills: selected, files: hashes, provenance: bundledProvenance(hashes) };
47
+ files.set('kit-manifest.json', Buffer.from(JSON.stringify(manifest, null, 2) + '\n'));
77
48
  const pending = [];
78
49
  for (const [relative, data] of files) {
79
50
  const target = path.join(destination, relative);
80
51
  checkPath(target);
81
52
  const info = stat(target);
82
53
  if (info) {
54
+ // Legacy manifests remain byte-for-byte intact on an otherwise identical init.
55
+ if (relative === 'kit-manifest.json' && info.isFile()) {
56
+ const parsed = parseJson(fs.readFileSync(target, 'utf8'));
57
+ const existing = parsed;
58
+ if (existing && typeof existing === 'object' && existing.provenance === undefined && existing.format === 2 && existing.kit === manifest.kit && existing.tool === manifest.tool &&
59
+ JSON.stringify(existing.skills) === JSON.stringify(selected) &&
60
+ Object.keys(existing.files ?? {}).length === Object.keys(hashes).length &&
61
+ Object.entries(hashes).every(([name, hash]) => existing.files?.[name] === hash))
62
+ continue;
63
+ }
83
64
  if (!info.isFile() || !fs.readFileSync(target).equals(data))
84
65
  throw new Error(`Conflict; no files written: ${relative}`);
85
66
  }
@@ -120,3 +101,26 @@ export function initialize(options) {
120
101
  }
121
102
  return { destination, tool: options.tool, skills: selected, files: files.size, new: pending.length, identical: files.size - pending.length, dryRun: Boolean(options.dryRun) };
122
103
  }
104
+ /** The exact host-profiled payload shipped with this CLI. */
105
+ export function bundledFiles(tool, selected) {
106
+ const files = new Map();
107
+ for (const name of selected) {
108
+ const source = path.join(packageRoot, '.agents/skills', name);
109
+ checkPath(source);
110
+ for (const relative of walk(source)) {
111
+ if (!/^(SKILL\.md|assets\/.*\.md|references\/.*\.md)$/.test(relative))
112
+ throw new Error(`Unexpected payload file: ${name}/${relative}`);
113
+ files.set(`${tools[tool]}/${name}/${relative}`, profile(fs.readFileSync(path.join(source, relative)), tool));
114
+ }
115
+ }
116
+ for (const template of templates)
117
+ files.set(template, profile(fs.readFileSync(path.join(packageRoot, '.agents/skills/project-foundation/assets', template)), tool));
118
+ files.set('DEVMETHOD-LICENSE', fs.readFileSync(path.join(packageRoot, 'LICENSE')));
119
+ return files;
120
+ }
121
+ export function bundledProvenance(hashes) {
122
+ const metadata = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
123
+ const canonical = Object.keys(hashes).sort().map(name => [name, hashes[name]]);
124
+ return { packageName: metadata.name, packageVersion: metadata.version,
125
+ payloadSha256: createHash('sha256').update(JSON.stringify(canonical)).digest('hex') };
126
+ }
@@ -0,0 +1,85 @@
1
+ import { object, text, id, safePath, secretPath, readLocal, digest, hash, gitState, validGit } from './records.js';
2
+ const list = (v) => Array.isArray(v) && v.length <= 256 && v.every(text);
3
+ const oneOf = (v, values) => typeof v === 'string' && values.includes(v);
4
+ export function validateMission(v) {
5
+ if (!object(v) || v.format !== 1 || !id(v.id) || !oneOf(v.path, ['quick', 'standard', 'major']) || !text(v.outcome)
6
+ || !text(v.owner) || !oneOf(v.status, ['active', 'blocked', 'complete']))
7
+ throw new Error('Mission format 1 requires id, path, outcome, owner and status.');
8
+ for (const key of ['scope', 'exclusions', 'invariants', 'uncertainties', 'stopConditions'])
9
+ if (!list(v[key]))
10
+ throw new Error(`Mission ${key} must be a bounded string array.`);
11
+ if (!v.scope.length || !v.stopConditions.length)
12
+ throw new Error('Scope and stop conditions cannot be empty.');
13
+ if (v.status === 'complete' ? v.nextAction !== null : !text(v.nextAction))
14
+ throw new Error('Active/blocked mission needs nextAction; complete requires null.');
15
+ if (!Array.isArray(v.sources) || v.sources.length === 0 || v.sources.length > 64 || !v.sources.every(s => object(s)
16
+ && id(s.id) && safePath(s.path) && !secretPath(s.path) && oneOf(s.level, ['project', 'domain', 'mission', 'execution'])
17
+ && oneOf(s.kind, ['fact', 'assumption', 'proposal', 'accepted-decision']) && text(s.reason) && text(s.authority) && text(s.revision)))
18
+ throw new Error('Select 1–64 safe sources with level, reason, authority, kind and revision; secret paths are excluded.');
19
+ if (!Array.isArray(v.acceptance) || !v.acceptance.length || v.acceptance.length > 128 || !v.acceptance.every(c => object(c)
20
+ && id(c.id) && text(c.description) && list(c.changes) && c.changes.length > 0 && c.changes.every(safePath)
21
+ && text(c.verification) && oneOf(c.kind, ['automated', 'manual', 'design-review', 'recommendation'])))
22
+ throw new Error('Acceptance criteria require IDs, changes, verification and verification kind.');
23
+ if (!Array.isArray(v.dependencies) || v.dependencies.length > 128 || !v.dependencies.every(d => object(d) && id(d.id) && text(d.owner) && text(d.detail) && oneOf(d.status, ['resolved', 'blocked'])))
24
+ throw new Error('Dependencies require id, owner, detail and resolved/blocked status.');
25
+ const mission = v;
26
+ const sourceIds = new Set(mission.sources.map(s => s.id));
27
+ if (sourceIds.size !== mission.sources.length || new Set(mission.sources.map(s => s.path)).size !== mission.sources.length
28
+ || new Set(mission.acceptance.map(c => c.id)).size !== mission.acceptance.length
29
+ || new Set(mission.dependencies.map(d => d.id)).size !== mission.dependencies.length)
30
+ throw new Error('IDs and source paths must be unique.');
31
+ if (!Array.isArray(v.contradictions) || v.contradictions.length > 128 || !v.contradictions.every(c => object(c)
32
+ && list(c.sourceIds) && c.sourceIds.length >= 2 && c.sourceIds.every(s => sourceIds.has(s)) && text(c.detail) && typeof c.resolved === 'boolean'))
33
+ throw new Error('Contradictions require at least two selected source IDs, detail and resolved boolean.');
34
+ return { format: 1, id: mission.id, path: mission.path, outcome: mission.outcome,
35
+ scope: [...mission.scope], exclusions: [...mission.exclusions], invariants: [...mission.invariants], uncertainties: [...mission.uncertainties],
36
+ owner: mission.owner, status: mission.status, nextAction: mission.nextAction, stopConditions: [...mission.stopConditions],
37
+ acceptance: mission.acceptance.map(c => ({ id: c.id, description: c.description, changes: [...c.changes], verification: c.verification, kind: c.kind })),
38
+ sources: mission.sources.map(s => ({ id: s.id, path: s.path, level: s.level, reason: s.reason, authority: s.authority, kind: s.kind, revision: s.revision })),
39
+ dependencies: mission.dependencies.map(d => ({ id: d.id, owner: d.owner, status: d.status, detail: d.detail })),
40
+ contradictions: mission.contradictions.map(c => ({ sourceIds: [...c.sourceIds], detail: c.detail, resolved: c.resolved })) };
41
+ }
42
+ export function missionStatus(mission) {
43
+ if (mission.status === 'blocked' || mission.dependencies.some(d => d.status === 'blocked') || mission.contradictions.some(c => !c.resolved))
44
+ return 'blocked';
45
+ return mission.status === 'complete' ? 'complete' : 'ready';
46
+ }
47
+ function sourceHash(root, file) {
48
+ const bytes = readLocal(root, file, 256 * 1024);
49
+ const body = bytes.toString('utf8');
50
+ if (body.includes('\0') || /-----BEGIN [A-Z ]*PRIVATE KEY-----|(?:api[_-]?key|password|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-/+]{20,}/i.test(body))
51
+ throw new Error(`Potential credential or binary source excluded: ${file}`);
52
+ return digest(bytes);
53
+ }
54
+ /** Metadata only; source text and command execution are deliberately excluded. */
55
+ export function captureContext(root, input) {
56
+ const mission = validateMission(input);
57
+ return { format: 1, mission, git: gitState(root), sources: mission.sources.map(s => ({ ...s, sha256: sourceHash(root, s.path) })) };
58
+ }
59
+ export function inspectContext(root, input) {
60
+ if (!object(input) || input.format !== 1 || !validGit(input.git))
61
+ throw new Error('Expected context format 1 with Git provenance.');
62
+ const mission = validateMission(input.mission);
63
+ if (!Array.isArray(input.sources) || input.sources.length !== mission.sources.length)
64
+ throw new Error('Context pins must match mission selection.');
65
+ const pins = input.sources;
66
+ for (let i = 0; i < pins.length; i++) {
67
+ const pin = pins[i];
68
+ if (!object(pin) || !hash(pin.sha256) || Object.entries(mission.sources[i]).some(([k, value]) => pin[k] !== value))
69
+ throw new Error('Context source metadata differs from mission selection.');
70
+ }
71
+ const sources = mission.sources.map((s, i) => {
72
+ try {
73
+ return { id: s.id, state: sourceHash(root, s.path) === pins[i].sha256 ? 'unchanged' : 'changed' };
74
+ }
75
+ catch {
76
+ return { id: s.id, state: 'unavailable' };
77
+ }
78
+ });
79
+ const current = gitState(root);
80
+ const gitChanged = Object.entries(input.git).some(([key, value]) => current[key] !== value);
81
+ const stale = gitChanged || sources.some(s => s.state !== 'unchanged');
82
+ return { format: 1, missionId: mission.id, status: missionStatus(mission) === 'blocked' ? 'blocked' : stale ? 'reverify' : missionStatus(mission),
83
+ sources, gitChanged, currentGit: current, nextAction: mission.nextAction,
84
+ limitations: 'Explicit pins only. Git changes require reassessment; semantic contradictions, external state and untracked file contents need manual review. No execution authorization.' };
85
+ }
@@ -0,0 +1,65 @@
1
+ import { object, text, id, safePath } from './records.js';
2
+ const strings = (v) => Array.isArray(v) && v.length <= 128 && v.every(text) && new Set(v).size === v.length;
3
+ const overlaps = (a, b) => a.toLowerCase() === b.toLowerCase() || a.toLowerCase().startsWith(b.toLowerCase() + '/') || b.toLowerCase().startsWith(a.toLowerCase() + '/');
4
+ export function inspectPlan(input) {
5
+ if (!object(input) || input.format !== 1 || typeof input.status !== 'string' || !['active', 'complete', 'cancelled'].includes(input.status)
6
+ || ![1, 2].includes(input.concurrency) || !text(input.contractOwner) || !strings(input.sharedContracts) || !input.sharedContracts.every(safePath)
7
+ || !Array.isArray(input.tasks) || !input.tasks.length || input.tasks.length > 128)
8
+ throw new Error('Expected bounded plan format 1, concurrency 1 or 2, contractOwner, sharedContracts and 1–128 tasks.');
9
+ for (const t of input.tasks)
10
+ if (!object(t) || !id(t.id) || !text(t.owner) || !safePath(t.worktree)
11
+ || !strings(t.owns) || !t.owns.length || !t.owns.every(safePath) || !strings(t.dependsOn) || !t.dependsOn.every(id)
12
+ || !strings(t.acceptance) || !t.acceptance.length || !safePath(t.checkpoint)
13
+ || typeof t.status !== 'string' || !['pending', 'running', 'passed', 'failed', 'blocked', 'cancelled'].includes(t.status)
14
+ || !Number.isInteger(t.attempts) || t.attempts < 0 || !Number.isInteger(t.maxAttempts) || t.maxAttempts < 1 || t.maxAttempts > 10 || t.attempts > t.maxAttempts
15
+ || typeof t.evidence !== 'string' || !['current', 'stale', 'missing'].includes(t.evidence))
16
+ throw new Error('Invalid task: require ownership, isolated worktree, dependencies, acceptance, bounded attempts, checkpoint and evidence state.');
17
+ const plan = input;
18
+ const byId = new Map(plan.tasks.map(t => [t.id, t]));
19
+ if (byId.size !== plan.tasks.length)
20
+ throw new Error('Duplicate task ID.');
21
+ for (const t of plan.tasks) {
22
+ if (t.dependsOn.some(d => !byId.has(d)))
23
+ throw new Error('Unknown dependency.');
24
+ if (t.owns.some(p => plan.sharedContracts.some(c => overlaps(p, c))) && t.owner !== plan.contractOwner)
25
+ throw new Error('Shared contracts require the single contract owner.');
26
+ if (['running', 'passed', 'failed'].includes(t.status) && t.attempts === 0)
27
+ throw new Error('Running, passed or failed tasks require a recorded attempt.');
28
+ }
29
+ for (let i = 0; i < plan.tasks.length; i++)
30
+ for (const b of plan.tasks.slice(i + 1)) {
31
+ const a = plan.tasks[i];
32
+ if (overlaps(a.worktree, b.worktree))
33
+ throw new Error('Worktrees must be distinct and non-nested.');
34
+ if (a.owns.some(p => b.owns.some(q => overlaps(p, q))))
35
+ throw new Error('Task ownership must not overlap, even across dependent tasks.');
36
+ }
37
+ const done = new Set();
38
+ while (done.size < plan.tasks.length) {
39
+ let progress = false;
40
+ for (const t of plan.tasks)
41
+ if (!done.has(t.id) && t.dependsOn.every(d => done.has(d))) {
42
+ done.add(t.id);
43
+ progress = true;
44
+ }
45
+ if (!progress)
46
+ throw new Error('Task dependency cycle.');
47
+ }
48
+ const results = new Map();
49
+ for (const key of done) {
50
+ const t = byId.get(key);
51
+ results.set(key, t.status === 'running' ? 'needs-reconciliation' : t.status === 'passed' ?
52
+ t.evidence === 'current' && t.dependsOn.every(d => results.get(d) === 'passed') ? 'passed' : 'blocked' :
53
+ t.status !== 'pending' ? t.status : t.attempts >= t.maxAttempts ? 'exhausted' : t.evidence === 'stale' ? 'blocked' :
54
+ t.dependsOn.every(d => results.get(d) === 'passed') ? 'eligible' : 'blocked');
55
+ }
56
+ const running = plan.tasks.filter(t => t.status === 'running').length;
57
+ if (running > plan.concurrency)
58
+ throw new Error('Running tasks exceed concurrency.');
59
+ if (plan.status === 'complete' && [...results.values()].some(s => s !== 'passed'))
60
+ throw new Error('Completed plan requires current passing evidence for every task.');
61
+ const candidates = plan.status === 'active' && running === 0 ? plan.tasks.filter(t => results.get(t.id) === 'eligible').slice(0, plan.concurrency).map(t => t.id) : [];
62
+ return { format: 1, status: plan.status, tasks: plan.tasks.map(t => ({ id: t.id, state: results.get(t.id) })), candidates,
63
+ adapter: 'manual-planning-only', dispatch: 'not-implemented',
64
+ limitations: 'Claims are supplied by the operator; validate checkpoint evidence and actual isolated worktrees before use. Running tasks require reconciliation. No retries, host dispatch or new authorization.' };
65
+ }
@@ -0,0 +1,76 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { parseJson, checkPath } from './filesystem.js';
6
+ export const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
7
+ export const text = (v) => typeof v === 'string' && v.trim().length > 0 && v.length <= 8192;
8
+ export const id = (v) => typeof v === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(v);
9
+ export const digest = (v) => createHash('sha256').update(v).digest('hex');
10
+ export const hash = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
11
+ export function safePath(v) {
12
+ return typeof v === 'string' && v.length > 0 && v.length <= 1024 && !/[\\:\x00-\x1f\x7f]/.test(v)
13
+ && v.split('/').every(p => p.length > 0 && p !== '.' && p !== '..' && !/[. ]$/.test(p)
14
+ && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(p));
15
+ }
16
+ export const secretPath = (v) => /(^|\/)(\.env(?:\..*)?|\.git|\.ssh|\.npmrc|credentials(?:\..*)?|secrets?(?:\..*)?)(\/|$)|\.(pem|key|p12|pfx)$/i.test(v);
17
+ export function readLocal(root, file, limit = 1024 * 1024) {
18
+ if (!safePath(file))
19
+ throw new Error('Expected a safe repository-relative path.');
20
+ const target = path.resolve(root, file);
21
+ checkPath(target);
22
+ const info = fs.lstatSync(target);
23
+ if (!info.isFile() || info.size > limit)
24
+ throw new Error(`Expected regular file within ${limit} bytes: ${file}`);
25
+ return fs.readFileSync(target);
26
+ }
27
+ export function readRecord(root, file) {
28
+ if (secretPath(file))
29
+ throw new Error('Secret-like record paths are excluded.');
30
+ const bytes = readLocal(root, file);
31
+ return parseJson(bytes.toString('utf8'));
32
+ }
33
+ function git(root, args) {
34
+ checkPath(path.resolve(root));
35
+ return execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-C', path.resolve(root), ...args], {
36
+ encoding: 'utf8', timeout: 10000, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
37
+ env: { ...process.env, GIT_PAGER: 'cat', GIT_TERMINAL_PROMPT: '0' }
38
+ });
39
+ }
40
+ export function gitState(root) {
41
+ // Avoid status/diff: repository-configured clean filters can execute even during reads.
42
+ // ls-files reads index metadata; hash raw tracked bytes ourselves, without conversion.
43
+ const index = git(root, ['ls-files', '--stage', '-z']);
44
+ const tracked = git(root, ['ls-files', '-z']).split('\0').filter(Boolean);
45
+ if (tracked.length > 10000)
46
+ throw new Error('Git provenance exceeds 10000 tracked files.');
47
+ let bytesRead = 0;
48
+ const working = tracked.map(file => {
49
+ if (!safePath(file) || secretPath(file))
50
+ return [file, 'excluded'];
51
+ const target = path.resolve(root, file);
52
+ checkPath(target);
53
+ if (!fs.existsSync(target))
54
+ return [file, 'missing'];
55
+ const info = fs.lstatSync(target);
56
+ if (info.isDirectory())
57
+ return [file, 'directory-or-submodule'];
58
+ bytesRead += info.size;
59
+ if (bytesRead > 64 * 1024 * 1024)
60
+ throw new Error('Git provenance exceeds 64 MiB of tracked content; use selected checkpoint pins instead.');
61
+ return [file, digest(readLocal(root, file, 8 * 1024 * 1024)), info.mode & 0o111];
62
+ });
63
+ const untracked = git(root, ['ls-files', '--others', '--exclude-standard', '-z']);
64
+ return { branch: git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim(), commit: git(root, ['rev-parse', 'HEAD']).trim(),
65
+ statusSha256: digest(index + '\0' + untracked), diffSha256: digest(JSON.stringify(working)) };
66
+ }
67
+ export function validGit(v) {
68
+ return object(v) && text(v.branch) && typeof v.commit === 'string' && /^[a-f0-9]{40,64}$/.test(v.commit)
69
+ && hash(v.statusSha256) && hash(v.diffSha256);
70
+ }
71
+ export function discover(root) {
72
+ const files = git(root, ['ls-files', '-z']).split('\0').filter(v => safePath(v) && !secretPath(v));
73
+ if (files.length > 10000)
74
+ throw new Error('Discovery exceeds 10000 tracked paths; select sources explicitly.');
75
+ return files;
76
+ }
package/dist/update.js ADDED
@@ -0,0 +1,57 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { bundledFiles, bundledProvenance } from './init.js';
5
+ import { diagnose, validateManifest } from './doctor.js';
6
+ import { parseJson, checkPath, stat } from './filesystem.js';
7
+ const hash = (data) => createHash('sha256').update(data).digest('hex');
8
+ /** Preview only. No staging writes, migrations, subprocesses, or network calls. */
9
+ export function previewUpdate(destination) {
10
+ const report = { format: 1, destination: path.resolve(destination), status: 'ok', provenance: 'unknown', entries: [], findings: [] };
11
+ try {
12
+ const diagnostics = diagnose(report.destination);
13
+ report.findings.push(...diagnostics.findings);
14
+ report.status = diagnostics.status;
15
+ // Missing files are represented in the comparison, but remain diagnostic errors.
16
+ if (diagnostics.findings.some(f => f.severity === 'error' && f.code !== 'file-missing'))
17
+ return report;
18
+ const manifestPath = path.join(report.destination, 'kit-manifest.json');
19
+ checkPath(manifestPath);
20
+ const info = stat(manifestPath);
21
+ if (!info?.isFile() || info.size > 1024 * 1024)
22
+ throw new Error('Invalid manifest file.');
23
+ const manifest = validateManifest(parseJson(fs.readFileSync(manifestPath, 'utf8')));
24
+ report.installed = manifest.provenance;
25
+ report.provenance = manifest.provenance ? 'recorded' : 'unknown';
26
+ if (!manifest.provenance) {
27
+ if (report.status === 'ok')
28
+ report.status = 'warning';
29
+ report.findings.push({ code: 'provenance-unknown', message: 'Legacy installation: package version is unknown; recorded file hashes remain the comparison baseline.' });
30
+ }
31
+ const candidate = bundledFiles(manifest.tool, manifest.skills);
32
+ const hashes = Object.fromEntries([...candidate].map(([name, data]) => [name, hash(data)]));
33
+ report.candidate = bundledProvenance(hashes);
34
+ for (const name of [...new Set([...Object.keys(manifest.files), ...candidate.keys()])].sort()) {
35
+ const baseline = manifest.files[name]?.toLowerCase();
36
+ const next = hashes[name];
37
+ const file = path.join(report.destination, name);
38
+ checkPath(file);
39
+ const current = stat(file);
40
+ if (current && !current.isFile())
41
+ throw new Error(`Expected a regular file: ${name}`);
42
+ const actual = current ? hash(fs.readFileSync(file)) : undefined;
43
+ const customized = baseline !== undefined && actual !== baseline;
44
+ const classification = baseline === undefined ? 'added' : customized && baseline !== next && actual !== next ? 'conflict' : customized ? 'customized' : next === undefined ? 'removed' : baseline === next ? 'unchanged' : 'updated';
45
+ const collision = baseline === undefined && actual !== undefined;
46
+ report.entries.push({ path: name, classification, installedSha256: actual, baselineSha256: baseline, candidateSha256: next,
47
+ candidateChanged: baseline !== next, ...(actual === undefined ? { missing: true } : {}), ...(collision ? { collision: true } : {}) });
48
+ if ((customized || collision) && report.status === 'ok')
49
+ report.status = 'warning';
50
+ }
51
+ }
52
+ catch (error) {
53
+ report.status = 'error';
54
+ report.findings.push({ code: 'preview-failed', message: error instanceof Error ? error.message : String(error) });
55
+ }
56
+ return report;
57
+ }
@@ -0,0 +1,13 @@
1
+ # ADR 001: Portable skill distribution
2
+
3
+ Status: accepted, 2026-09-12.
4
+
5
+ Distribute six Markdown skills with a strict TypeScript installer compiled to JavaScript. Require Node.js 22+ and npm, with no runtime dependencies. Store the source skills in `.agents/skills`; install one chosen host profile. Keep one copy of each skill per project to avoid ambiguous discovery. Commit dist/ alongside src/ so GitHub installation requires no build hooks. Use a lockfile for development dependencies.
6
+
7
+ The fourteen workflow stages are arguments to the project-foundation skill. They are model instructions, not deterministic executable commands. Native host permissions and project decisions remain authoritative. A next-command suggestion never grants execution permission.
8
+
9
+ Claude Code and Cursor each have a native installation profile. Format compliance and authenticated behavioral validation are separate gates. Personal interface metadata and icons are excluded. Installation conflicts fail before any file is written; existing project instructions must be merged intentionally.
10
+
11
+ The init command chooses a host and optional modules, always includes project-foundation, and defaults to the current project. Non-interactive calls require an explicit host. Include a separately named MIT notice and reject symbolic paths and duplicate host copies. Install into directories that are not being concurrently modified: rollback covers files and directories created by this process, not concurrent external changes or process termination.
12
+
13
+ Installation is offline after npm obtains the package. No telemetry, model call, credential storage or deployment is introduced. GitHub installation and npm registry publication are separate; registry publication requires maintainer authentication. Upgrade through a staging directory and intentional review of differences. Project context is never migrated automatically.
@@ -0,0 +1,25 @@
1
+ # ADR 002: Read-only installation diagnostics
2
+
3
+ Status: proposed for maintainer review with this implementation.
4
+
5
+ ## Problem
6
+
7
+ `init` safely refuses divergent files, but cannot explain whether an adopted installation is missing files, intentionally customized, or duplicated across hosts. Users need this diagnosis before comparing an update.
8
+
9
+ ## Proposed contract
10
+
11
+ Add `devmethod doctor [--dest PATH] [--json]`. Infer the host from the existing format 2 manifest. Keep the existing manifest format and support installations made before this command. Do not compare an older installation against the current package's entire file list: that would confuse an upgrade with corruption.
12
+
13
+ Validate the manifest structure and permitted paths before reading payload files. Require the root templates, license and selected module entry points in the manifest. Inspect recorded SHA-256 hashes, missing or non-regular files, symbolic paths and duplicate selected modules in other host directories. No writes, subprocesses, repairs, network access, or host execution.
14
+
15
+ JSON report format 1 contains destination, status, optional inferred host/modules, checked/unchanged counts and findings with severity, code, optional path, and message. Codes identify categories; message wording may evolve. Exit 0 for a clean or customized installation, 1 for diagnostic errors, and 2 for invalid CLI invocation. Automation that requires pristine files must also inspect `status`, since customization is an expected warning.
16
+
17
+ ## Alternatives and limits
18
+
19
+ Manual directory comparison remains useful for updates but does not offer a stable machine-readable diagnosis. Automatic repair would need a separate migration and ownership contract; it is excluded. Adding configuration/state engines is unnecessary for this diagnostic slice.
20
+
21
+ Manifest hashes compare with a local baseline, not a signed release. Someone who changes both files and manifest can conceal changes. Doctor does not certify host discovery, prompt behavior, workflow gates, project readiness, or authenticity. It does not inventory unrecorded payload files or discover deleted optional entries in a modified manifest. Use a reviewed staging installation for that comparison. As with init, inspect a directory that is not being concurrently modified; symlink preflight is not an operating-system sandbox.
22
+
23
+ ## Validation
24
+
25
+ Test all three host layouts and subsets, byte preservation, customized templates/skills, missing files, duplicate hosts, invalid manifests, traversal and symbolic paths, JSON output and exit codes. Run the existing installer tests after extracting shared filesystem checks. Exercise the command from the actual packed tarball. Native coding-agent validation remains a separate gate.
@@ -0,0 +1,15 @@
1
+ # ADR 003: Local provenance and read-only update previews
2
+
3
+ Status: proposed for maintainer review with this implementation.
4
+
5
+ ## Context and decision
6
+
7
+ An installed format 2 manifest records file hashes but not the package version. Add optional package name/version and a deterministic host/subset payload digest without changing the format or rewriting legacy manifests. The bundled package metadata is the source of the new installation's version; an old installation without this record remains unknown.
8
+
9
+ Add `update-preview` to compare recorded baseline, local bytes and the current CLI's bundled payload. Reuse manifest path validation and diagnostics before reading payload. Keep the operation offline and read-only, with no runtime dependencies. Preserve all six modules, host profiling and subset semantics. Classification and hashes expose upstream differences separately from local customization; no automatic overwrite, deletion or context migration is introduced.
10
+
11
+ ## Consequences and limits
12
+
13
+ Version alone cannot establish payload equality; the digest includes selected host-profiled contents. Neither version nor digest establishes authenticity because manifests and package files are locally mutable. Missing optional historical entries cannot be reconstructed. Local changes to removed files stay classified as customization. Legacy idempotence preserves the original manifest only when its installation contract matches the requested payload. Content review uses a separate fresh staging installation and intentional diff.
14
+
15
+ The filesystem contract inherits ADR 001 and ADR 002: reject symlinks and unsupported paths, assume no concurrent modification, and distinguish document/integrity checks from native-host behavior. Tests cover all host layouts and subsets, legacy/new idempotence, classification, preservation, path attacks and CLI results.
@@ -0,0 +1,23 @@
1
+ # ADR 004: Content-pinned checkpoint evidence
2
+
3
+ Status: proposed for maintainer review with this implementation, 2026-09-13.
4
+
5
+ ## Decision and scope
6
+
7
+ The authorized checkpoint-resumption milestone needs a bounded way to detect when previous verification no longer applies. Preserve the accepted six-module distribution, offline core and existing Markdown checkpoint workflow. Add an optional format 1 JSON record and dependency-free read-only API, owned by the checkpoint slice.
8
+
9
+ Pin repository-relative source files and evidence artifacts with SHA-256. Link each evidence item to explicit source IDs and optional evidence prerequisites. Validate references and reject cycles before inspection. Propagate invalidation from changed or unavailable sources and artifacts to their dependent evidence; preserve independent results. Failed and unrun prerequisites cannot support downstream success. Ignore age as an invalidation signal.
10
+
11
+ Keep scope/status/next action explicit. Completed scope requires a null next action. Inspection produces a report, never a state transition, automatic rehash, test execution, migration or new authorization. Blocked scope remains blocked until an authorized owner resolves it. Existing Markdown checkpoints continue manually; JSON is an opt-in companion rather than a replacement tracker.
12
+
13
+ ## Alternatives and consequences
14
+
15
+ Keeping only prose is simplest but cannot reliably map changed inputs to affected evidence. Commit-only invalidation is too broad for independent work and misses uncommitted changes. Modification time confuses age with changed content. A scheduler or automatic repair system would expand permissions and operating complexity beyond this milestone.
16
+
17
+ Explicit pins require authors to record complete dependencies. The inspector cannot certify honest evidence, external state or omitted inputs. Local hashes are not release signatures. Unknown schema versions fail with actionable diagnostics. Portable safe paths and symlink preflight bound ordinary reads but do not defend against concurrent filesystem replacement. Inspect a quiescent project directory.
18
+
19
+ Revisit the schema only after native resumption runs expose concrete missing contracts. New schema versions must retain a deliberate compatibility path; this slice does not mandate adoption by existing projects.
20
+
21
+ ## Verification
22
+
23
+ Tests cover selective/transitive invalidation, changed artifacts, failed and unrun prerequisites, unchanged bytes with old timestamps, absent files, malformed records, unsafe/symbolic paths and complete/blocked scope. Full installer tests and package inspection remain required. These tests are deterministic validator evidence, never native host behavior evidence.