devmethod-ai 0.1.0-rc.2 → 0.1.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 (172) 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 +11 -10
  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/project-foundation/SKILL.md +39 -30
  10. package/.agents/skills/project-foundation/assets/AGENTS.foundation.md +16 -15
  11. package/.agents/skills/project-foundation/assets/PROJECT_PROFILE.md +23 -23
  12. package/.agents/skills/project-foundation/assets/START_HERE.md +17 -14
  13. package/.agents/skills/project-foundation/references/mission-context.md +20 -0
  14. package/.agents/skills/project-foundation/references/operating-commands.md +37 -34
  15. package/.agents/skills/project-foundation/references/profiles/README.md +18 -0
  16. package/.agents/skills/project-foundation/references/profiles/ci.md +11 -0
  17. package/.agents/skills/project-foundation/references/profiles/cloud-delivery.md +13 -0
  18. package/.agents/skills/project-foundation/references/profiles/messaging.md +11 -0
  19. package/.agents/skills/project-foundation/references/profiles/mongodb.md +11 -0
  20. package/.agents/skills/project-foundation/references/profiles/node-nest.md +11 -0
  21. package/.agents/skills/project-foundation/references/profiles/postgres-drizzle.md +11 -0
  22. package/.agents/skills/project-foundation/references/profiles/react-next.md +11 -0
  23. package/.agents/skills/project-foundation/references/profiles/typescript.md +11 -0
  24. package/.agents/skills/project-foundation/references/work-sizing.md +31 -0
  25. package/.agents/skills/react-feature-engineering/SKILL.md +26 -22
  26. package/.agents/skills/react-feature-engineering/references/review-and-sources.md +41 -37
  27. package/.agents/skills/reliable-ai-integration/SKILL.md +13 -10
  28. package/.agents/skills/reliable-ai-integration/assets/AI_EVALUATION.md +19 -18
  29. package/.agents/skills/reliable-ai-integration/references/evidence-and-media.md +22 -18
  30. package/.agents/skills/reliable-ai-integration/references/jobs-and-costs.md +20 -15
  31. package/.agents/skills/scoped-delivery/SKILL.md +23 -18
  32. package/.agents/skills/scoped-delivery/assets/CHECKPOINT.md +22 -11
  33. package/.agents/skills/scoped-delivery/assets/MISSION.md +18 -0
  34. package/.agents/skills/scoped-delivery/assets/SLICE.md +15 -14
  35. package/.agents/skills/scoped-delivery/assets/VERIFICATION.md +19 -0
  36. package/.agents/skills/scoped-delivery/references/verification-and-cost.md +21 -18
  37. package/COMPATIBILITY.md +3 -3
  38. package/CONTRIBUTING.md +13 -0
  39. package/README.md +74 -8
  40. package/START_HERE.md +24 -0
  41. package/VALIDATION.md +37 -0
  42. package/dist/checkpoint.js +171 -0
  43. package/dist/cli.js +118 -17
  44. package/dist/doctor.js +113 -0
  45. package/dist/filesystem.js +32 -0
  46. package/dist/init.js +38 -34
  47. package/dist/mission.js +85 -0
  48. package/dist/planner.js +65 -0
  49. package/dist/records.js +76 -0
  50. package/dist/update.js +57 -0
  51. package/docs/ADR-001-distribution.md +13 -0
  52. package/docs/ADR-002-read-only-diagnostics.md +25 -0
  53. package/docs/ADR-003-installation-provenance.md +15 -0
  54. package/docs/ADR-004-checkpoint-resumption.md +23 -0
  55. package/docs/ADR-005-mission-context.md +11 -0
  56. package/docs/ADR-006-opt-in-native-pilot.md +15 -0
  57. package/docs/BMAD-COMPARISON.md +34 -0
  58. package/docs/CODEX-ADAPTER-VALIDATION.md +20 -0
  59. package/docs/COMPARISON-ITERATION.md +25 -0
  60. package/docs/DOCTOR.md +23 -0
  61. package/docs/EVALUATION.md +39 -0
  62. package/docs/EVOLUTION.md +34 -0
  63. package/docs/GREENFIELD-VALIDATION.md +36 -0
  64. package/docs/MILESTONES.md +39 -0
  65. package/docs/MISSIONS.md +45 -0
  66. package/docs/NATIVE-PILOT-RESULTS.md +22 -0
  67. package/docs/NATIVE-PILOT.md +17 -0
  68. package/docs/ORCHESTRATION.md +11 -0
  69. package/docs/RC2-VALIDATION.md +50 -0
  70. package/docs/RELEASE-0.1.0.md +49 -0
  71. package/docs/RELEASE-CHECKLIST.md +13 -0
  72. package/docs/RELEASE-NOTES-0.1.0-rc.2.md +32 -0
  73. package/docs/RELEASE-NOTES-0.1.0.md +11 -0
  74. package/docs/RESUMPTION.md +87 -0
  75. package/docs/ROADMAP.md +37 -0
  76. package/docs/STACK-PROFILES.md +9 -0
  77. package/docs/TROUBLESHOOTING.md +16 -0
  78. package/docs/UPDATES.md +30 -0
  79. package/docs/images/devmethod-flow.svg +26 -0
  80. package/evaluation/COMPARISONS.md +33 -0
  81. package/evaluation/README.md +35 -0
  82. package/evaluation/candidate-setup.json +33 -0
  83. package/evaluation/comparison-v2-readiness.json +18 -0
  84. package/evaluation/comparison-v2-results.json +58 -0
  85. package/evaluation/fixtures/dependency/CONTRIBUTING.md +3 -0
  86. package/evaluation/fixtures/dependency/TICKET.md +3 -0
  87. package/evaluation/fixtures/dependency/acceptance.test.mjs +4 -0
  88. package/evaluation/fixtures/dependency/api.mjs +1 -0
  89. package/evaluation/fixtures/failing-verification/CONTRIBUTING.md +3 -0
  90. package/evaluation/fixtures/failing-verification/acceptance.test.mjs +4 -0
  91. package/evaluation/fixtures/failing-verification/total.mjs +1 -0
  92. package/evaluation/fixtures/stale-handoff/CHECKPOINT.md +3 -0
  93. package/evaluation/fixtures/stale-handoff/CONTRACT.md +3 -0
  94. package/evaluation/fixtures/stale-handoff/CONTRIBUTING.md +3 -0
  95. package/evaluation/fixtures/stale-handoff/acceptance.test.mjs +4 -0
  96. package/evaluation/fixtures/stale-handoff/label.mjs +1 -0
  97. package/evaluation/fixtures/update-customization/README.md +13 -0
  98. package/evaluation/fixtures.json +85 -0
  99. package/evaluation/greenfield/acceptance.test.mjs +57 -0
  100. package/evaluation/greenfield/security.test.mjs +28 -0
  101. package/evaluation/native-pilot-results.json +155 -0
  102. package/examples/README.md +36 -0
  103. package/examples/bugfix/acceptance.test.mjs +17 -0
  104. package/examples/bugfix/page-size.mjs +4 -0
  105. package/examples/fullstack/CONTRACT.md +23 -0
  106. package/examples/fullstack/DEPENDENCIES.md +18 -0
  107. package/examples/fullstack/README.md +100 -0
  108. package/examples/fullstack/api/application/tasks.ts +13 -0
  109. package/examples/fullstack/api/domain/task.ts +12 -0
  110. package/examples/fullstack/api/http.ts +26 -0
  111. package/examples/fullstack/api/infrastructure/migrate.ts +15 -0
  112. package/examples/fullstack/api/infrastructure/postgres.ts +18 -0
  113. package/examples/fullstack/api/infrastructure/schema.ts +6 -0
  114. package/examples/fullstack/api/main.ts +17 -0
  115. package/examples/fullstack/api/tsconfig.json +17 -0
  116. package/examples/fullstack/compose.yaml +14 -0
  117. package/examples/fullstack/migrations/0000_tasks.sql +5 -0
  118. package/examples/fullstack/migrations/meta/_journal.json +13 -0
  119. package/examples/fullstack/mission.json +34 -0
  120. package/examples/fullstack/package-lock.json +2661 -0
  121. package/examples/fullstack/package.json +45 -0
  122. package/examples/fullstack/tests/e2e.test.cjs +54 -0
  123. package/examples/fullstack/tests/http.test.cjs +33 -0
  124. package/examples/fullstack/tests/postgres.test.cjs +29 -0
  125. package/examples/fullstack/tests/unit.test.cjs +20 -0
  126. package/examples/fullstack/tests/web-model.test.cjs +23 -0
  127. package/examples/fullstack/web/app/layout.tsx +4 -0
  128. package/examples/fullstack/web/app/page.tsx +11 -0
  129. package/examples/fullstack/web/features/tasks/components/task-list.tsx +11 -0
  130. package/examples/fullstack/web/features/tasks/hooks/use-task-filter.ts +7 -0
  131. package/examples/fullstack/web/features/tasks/model/tasks.ts +12 -0
  132. package/examples/fullstack/web/features/tasks/server/list-tasks.ts +8 -0
  133. package/examples/fullstack/web/next.config.mjs +2 -0
  134. package/examples/fullstack/web/tsconfig.json +36 -0
  135. package/examples/mission/CONTRACT.md +3 -0
  136. package/examples/mission/mission.json +18 -0
  137. package/examples/mission/plan.json +11 -0
  138. package/examples/pocket-tasks/BRIEF.md +21 -0
  139. package/examples/pocket-tasks/PROJECT_PROFILE.md +19 -0
  140. package/examples/pocket-tasks/README.md +43 -0
  141. package/examples/pocket-tasks/docs/ADR-001.md +7 -0
  142. package/examples/pocket-tasks/docs/HANDOFF.md +16 -0
  143. package/examples/pocket-tasks/docs/MISSION.md +15 -0
  144. package/examples/pocket-tasks/docs/VERIFICATION.md +58 -0
  145. package/examples/pocket-tasks/package.json +1 -0
  146. package/examples/pocket-tasks/public/app.js +84 -0
  147. package/examples/pocket-tasks/public/index.html +12 -0
  148. package/examples/pocket-tasks/public/styles.css +1 -0
  149. package/examples/pocket-tasks/server.mjs +79 -0
  150. package/examples/pocket-tasks/src/domain.mjs +31 -0
  151. package/examples/pocket-tasks/src/store.mjs +49 -0
  152. package/examples/pocket-tasks/tests/app.test.mjs +132 -0
  153. package/examples/pocket-tasks/tests/ui-errors.test.mjs +49 -0
  154. package/package.json +15 -3
  155. package/scripts/check-codex-git.mjs +20 -0
  156. package/scripts/check-docs.mjs +14 -0
  157. package/scripts/check-native-sandbox.mjs +11 -0
  158. package/scripts/comparison-readiness.mjs +26 -0
  159. package/scripts/comparison.mjs +42 -0
  160. package/scripts/evaluation.mjs +64 -0
  161. package/scripts/fullstack-mission.mjs +30 -0
  162. package/scripts/hosts/checks.mjs +13 -0
  163. package/scripts/hosts/codex-meter.mjs +17 -0
  164. package/scripts/hosts/codex-rpc.mjs +44 -0
  165. package/scripts/hosts/codex-task.mjs +29 -0
  166. package/scripts/hosts/codex.mjs +28 -0
  167. package/scripts/native-host.mjs +55 -0
  168. package/scripts/package-smoke.mjs +64 -0
  169. package/scripts/probe-codex-children.mjs +41 -0
  170. package/scripts/run-comparison-v2.mjs +45 -0
  171. package/scripts/run-native-pilot.mjs +76 -0
  172. package/scripts/summarize-native-pilot.mjs +7 -0
package/VALIDATION.md ADDED
@@ -0,0 +1,37 @@
1
+ # Release candidate validation
2
+
3
+ ## Diagnostics and workflow improvement candidate
4
+
5
+ Source change based on `758491c3e85621c6adae6b5c71d1b32c28a380af`; npm release pending. Local runtime: Linux x64, Node.js 24.19.0. The platform results below belong to the earlier candidate, not this change.
6
+
7
+ - `npm ci --ignore-scripts --prefer-offline`, `npm test` (strict TypeScript build and 16 passing tests), and `npm pack --dry-run` passed.
8
+ - The actual local tarball installed all three host profiles and their scoped-delivery subsets via offline npx. Packaged `doctor --json` returned healthy reports, and retained customized profiles with warning status.
9
+ - Doctor accepted a format 2 installation produced by the previous source CLI (29 recorded files), as well as the new payload (31 recorded files).
10
+ - Updated skill frontmatter passed the skill validator; local Markdown file links and `git diff --check` passed.
11
+ - The B1 exercise produced the expected two failing tests. A temporary reference implementation passed both, confirming fixture solvability. This was a fixture check, not an independent model or BMad benchmark.
12
+
13
+ The existing platform CI now also checks packaged diagnostics. Record that run's conclusions before claiming this change passed on macOS/Windows. Authenticated host behavior, matched comparative evaluations and npm publication remain separate pending gates. Review performed locally by the implementing agent; no independent review is claimed.
14
+
15
+ ## Earlier 0.1 candidate
16
+
17
+ Date: 2026-09-12. Candidate: 0.1.
18
+
19
+ Executed `npm test`: strict TypeScript build and 7 Node.js tests passed. Coverage: three host layouts, six skill names and relative references, SHA-256 manifests, MIT notice, selected modules, dry-run without writes, identical installation, conflict rejection before writes, preserved project instructions, invalid arguments, duplicate host detection, blocked paths and symlink rejection.
20
+
21
+ `npm pack` produced the compiled CLI and skill payload without runtime dependencies. An actual offline `npx --package=<local-tarball> devmethod init --tool claude` installed all six modules successfully. Installation never migrates existing project context; customized templates normally differ from initial manifest hashes.
22
+
23
+ These checks validate installation and documentation; they do not execute a Claude or Cursor model. Native authenticated evaluations remain pending, as documented in COMPATIBILITY.md.
24
+
25
+ ## Native operating-system results
26
+
27
+ [GitHub Actions run 34720121631](https://github.com/montassarkhalloufi/DevMethod/actions/runs/34720121631) completed successfully for commit `0790845289eccfe4aa3cfd0fe9e849eeca11b0a7` with Node.js 22.23.2 on all three runners:
28
+
29
+ | System | Architecture | Unit tests | Packaged npx installations |
30
+ |---|---|---|---|
31
+ | Ubuntu | x64 | 7 passed | Codex, Claude Code, Cursor passed |
32
+ | macOS | ARM64 | 7 passed | Codex, Claude Code, Cursor passed |
33
+ | Windows Server 2025 | x64 | 7 passed | Codex, Claude Code, Cursor passed |
34
+
35
+ Each job rebuilt strict TypeScript, verified the committed build, packed the actual package and invoked npx against that tarball for each host. It checked all six installed modules and the SHA-256 manifest. Installation paths included spaces. This establishes the tested runtime combinations, not every OS version or an authenticated host session. Windows shell commands used Git Bash; PowerShell-specific interaction was not evaluated.
36
+
37
+ Publication target: https://github.com/montassarkhalloufi/DevMethod. This report covers the 0.1 candidate source and local checks, not a stable release certification. Native authenticated compatibility remains pending.
@@ -0,0 +1,171 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { parseJson, checkPath } from './filesystem.js';
5
+ import { gitState, validGit } from './records.js';
6
+ const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ const identifier = (value) => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value);
8
+ const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
9
+ // Portable repository-relative paths only: no Windows aliases, drives or streams.
10
+ function safePath(value) {
11
+ return typeof value === 'string' && value.length > 0 && !/[\\:\x00-\x1f\x7f]/.test(value)
12
+ && value.split('/').every(part => part.length > 0 && part !== '.' && part !== '..'
13
+ && !/[. ]$/.test(part) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part));
14
+ }
15
+ function pinned(value) {
16
+ return object(value) && identifier(value.id) && safePath(value.path)
17
+ && typeof value.sha256 === 'string' && /^[a-f0-9]{64}$/.test(value.sha256);
18
+ }
19
+ function ids(value) {
20
+ return Array.isArray(value) && value.every(identifier) && new Set(value).size === value.length;
21
+ }
22
+ function validate(input) {
23
+ if (!object(input) || input.format !== 1)
24
+ throw new Error('Expected checkpoint format 1. Markdown checkpoints remain a manual workflow.');
25
+ if (!nonempty(input.scope) || typeof input.status !== 'string' || !['active', 'blocked', 'complete'].includes(input.status))
26
+ throw new Error('Checkpoint needs a nonempty scope and status active, blocked or complete.');
27
+ if (input.nextAction !== null && !nonempty(input.nextAction))
28
+ throw new Error('nextAction must be a nonempty string or null.');
29
+ if (input.status === 'complete' && input.nextAction !== null)
30
+ throw new Error('Completed scope must have nextAction null; completion authorizes no new work.');
31
+ if (input.status !== 'complete' && !nonempty(input.nextAction))
32
+ throw new Error('Active or blocked scope needs an explicit nextAction.');
33
+ if (!Array.isArray(input.sources) || !input.sources.every(pinned) || input.sources.length === 0)
34
+ throw new Error('sources must contain pinned IDs, safe relative paths and lowercase SHA-256 hashes.');
35
+ if (!Array.isArray(input.evidence) || !input.evidence.every(item => pinned(item) && object(item)
36
+ && ids(item.sourceIds) && item.sourceIds.length > 0 && ids(item.dependsOn)
37
+ && typeof item.outcome === 'string' && ['passed', 'failed', 'blocked', 'not-run'].includes(item.outcome)))
38
+ throw new Error('evidence needs pinned artifacts, sourceIds, dependsOn and outcome passed, failed or not-run.');
39
+ if (input.sources.length > 256 || input.evidence.length > 256)
40
+ throw new Error('Checkpoint supports at most 256 sources and evidence items.');
41
+ if (input.git !== undefined && !validGit(input.git))
42
+ throw new Error('Invalid Git checkpoint provenance.');
43
+ if (input.blockers !== undefined && (!Array.isArray(input.blockers) || !input.blockers.every(nonempty)))
44
+ throw new Error('blockers must be nonempty strings.');
45
+ for (const item of input.evidence) {
46
+ if (item.criterionIds !== undefined && (!ids(item.criterionIds) || !item.criterionIds.length))
47
+ throw new Error('criterionIds must be nonempty unique IDs.');
48
+ if (item.kind !== undefined && (typeof item.kind !== 'string' || !['automated', 'manual', 'design-review', 'recommendation'].includes(item.kind)))
49
+ throw new Error('Invalid verification kind.');
50
+ if (item.revision !== undefined && !nonempty(item.revision))
51
+ throw new Error('Invalid evidence revision.');
52
+ }
53
+ const state = input;
54
+ const sources = new Set(state.sources.map(source => source.id));
55
+ const evidence = new Map(state.evidence.map(item => [item.id, item]));
56
+ if (sources.size !== state.sources.length || evidence.size !== state.evidence.length)
57
+ throw new Error('Source IDs and evidence IDs must each be unique.');
58
+ if (new Set(state.sources.map(source => source.path)).size !== state.sources.length
59
+ || new Set(state.evidence.map(item => item.path)).size !== state.evidence.length)
60
+ throw new Error('Source paths and evidence paths must each be unique.');
61
+ for (const item of state.evidence) {
62
+ if (item.sourceIds.some(id => !sources.has(id)) || item.dependsOn.some(id => !evidence.has(id)))
63
+ throw new Error(`Evidence ${item.id} references an unknown source or evidence ID.`);
64
+ }
65
+ // Iterative topological traversal avoids stack exhaustion on an untrusted graph.
66
+ const done = new Set();
67
+ while (done.size < evidence.size) {
68
+ let progress = false;
69
+ for (const item of state.evidence)
70
+ if (!done.has(item.id) && item.dependsOn.every(id => done.has(id))) {
71
+ done.add(item.id);
72
+ progress = true;
73
+ }
74
+ if (!progress)
75
+ throw new Error('Evidence dependencies contain a cycle.');
76
+ }
77
+ return state;
78
+ }
79
+ function readPinned(destination, relative) {
80
+ if (!safePath(relative))
81
+ throw new Error('Expected a safe repository-relative path.');
82
+ const file = path.join(destination, relative);
83
+ checkPath(file);
84
+ if (!fs.lstatSync(file).isFile() || fs.lstatSync(file).size > 1024 * 1024)
85
+ throw new Error('Expected a regular file.');
86
+ return fs.readFileSync(file);
87
+ }
88
+ function emptyReport(destination) {
89
+ return { format: 1, destination: path.resolve(destination), status: 'invalid', findings: [], sources: [], evidence: [] };
90
+ }
91
+ /** Read-only inspection; report readiness never grants execution or integration permission. */
92
+ export function inspectCheckpoint(destination, input) {
93
+ const report = emptyReport(destination);
94
+ let state;
95
+ try {
96
+ state = validate(input);
97
+ checkPath(report.destination);
98
+ if (!fs.statSync(report.destination).isDirectory())
99
+ throw new Error('Destination must be a directory.');
100
+ }
101
+ catch (error) {
102
+ report.findings.push({ code: 'invalid-checkpoint', message: error.message });
103
+ return report;
104
+ }
105
+ report.scope = state.scope;
106
+ report.nextAction = state.nextAction;
107
+ const compare = (item, kind) => {
108
+ try {
109
+ const digest = createHash('sha256').update(readPinned(report.destination, item.path)).digest('hex');
110
+ if (digest === item.sha256)
111
+ return 'unchanged';
112
+ report.findings.push({ code: `${kind}-changed`, id: item.id, path: item.path, message: `Pinned ${kind} content changed; inspect the difference and repeat affected verification.` });
113
+ return 'changed';
114
+ }
115
+ catch (error) {
116
+ report.findings.push({ code: `${kind}-unavailable`, id: item.id, path: item.path, message: `Cannot inspect pinned ${kind}: ${error.message}` });
117
+ return 'unavailable';
118
+ }
119
+ };
120
+ report.sources = state.sources.map(source => ({ id: source.id, state: compare(source, 'source') }));
121
+ const sourceStates = new Map(report.sources.map(source => [source.id, source.state]));
122
+ const artifactStates = new Map(state.evidence.map(item => [item.id, compare(item, 'evidence')]));
123
+ const evidenceStates = new Map();
124
+ while (evidenceStates.size < state.evidence.length) {
125
+ for (const item of state.evidence) {
126
+ if (evidenceStates.has(item.id) || !item.dependsOn.every(id => evidenceStates.has(id)))
127
+ continue;
128
+ const invalidated = artifactStates.get(item.id) !== 'unchanged'
129
+ || item.sourceIds.some(id => sourceStates.get(id) !== 'unchanged')
130
+ || item.dependsOn.some(id => evidenceStates.get(id) !== 'valid');
131
+ const result = invalidated ? 'invalidated' : item.outcome === 'passed' ? 'valid' : item.outcome;
132
+ evidenceStates.set(item.id, result);
133
+ if (result !== 'valid')
134
+ report.findings.push({ code: `evidence-${result}`, id: item.id, path: item.path,
135
+ message: `Evidence ${item.id} is ${result}; it cannot support resumption until affected verification is recorded again.` });
136
+ }
137
+ }
138
+ report.evidence = state.evidence.map(item => ({ id: item.id, state: evidenceStates.get(item.id) }));
139
+ let gitChanged = false;
140
+ if (state.git) {
141
+ try {
142
+ const current = gitState(report.destination);
143
+ gitChanged = Object.entries(state.git).some(([key, value]) => current[key] !== value);
144
+ if (gitChanged)
145
+ report.findings.push({ code: 'git-changed', message: 'Branch, commit or worktree changed; reassess scope and omitted dependencies. Independent pins are retained.' });
146
+ }
147
+ catch {
148
+ gitChanged = true;
149
+ report.findings.push({ code: 'git-unavailable', message: 'Cannot compare recorded Git provenance.' });
150
+ }
151
+ }
152
+ for (const blocker of state.blockers ?? [])
153
+ report.findings.push({ code: 'dependency-blocked', message: blocker });
154
+ const needsVerification = gitChanged || report.sources.some(source => source.state !== 'unchanged')
155
+ || report.evidence.some(item => item.state !== 'valid') || report.evidence.length === 0;
156
+ if (!report.evidence.length)
157
+ report.findings.push({ code: 'evidence-empty', message: 'No evidence was pinned; establish verification before relying on this checkpoint.' });
158
+ report.status = state.status === 'blocked' || (state.blockers?.length ?? 0) > 0 || state.evidence.some(item => item.outcome === 'blocked') ? 'blocked' : needsVerification ? 'reverify' : state.status === 'complete' ? 'complete' : 'ready';
159
+ return report;
160
+ }
161
+ /** Load a JSON checkpoint within the project; never follows symbolic paths or changes files. */
162
+ export function readCheckpoint(destination, checkpointPath) {
163
+ try {
164
+ return inspectCheckpoint(destination, parseJson(readPinned(path.resolve(destination), checkpointPath).toString('utf8')));
165
+ }
166
+ catch (error) {
167
+ const report = emptyReport(destination);
168
+ report.findings.push({ code: 'invalid-checkpoint', path: checkpointPath, message: error.message });
169
+ return report;
170
+ }
171
+ }
package/dist/cli.js CHANGED
@@ -3,43 +3,144 @@ import { parseArgs } from 'node:util';
3
3
  import { createInterface } from 'node:readline/promises';
4
4
  import { stdin, stdout } from 'node:process';
5
5
  import { initialize, tools } from './init.js';
6
- const help = `DevMethod initialize a project with reusable AI skills
6
+ import { diagnose } from './doctor.js';
7
+ import { readCheckpoint } from './checkpoint.js';
8
+ import { previewUpdate } from './update.js';
9
+ import { readRecord, discover } from './records.js';
10
+ import { validateMission, missionStatus, captureContext, inspectContext } from './mission.js';
11
+ import { inspectPlan } from './planner.js';
12
+ const help = `DevMethod — install and inspect reusable AI skills
7
13
 
8
14
  devmethod init [--tool codex|claude|cursor] [--dest PATH]
9
15
  [--modules name,name] [--dry-run]
16
+ devmethod doctor [--dest PATH] [--json]
17
+ devmethod update-preview [--dest PATH] [--json]
18
+ devmethod mission --mission RELATIVE_JSON [--dest PATH] [--json]
19
+ devmethod context --mission RELATIVE_JSON [--dest PATH] [--json]
20
+ devmethod context-check --context RELATIVE_JSON [--dest PATH] [--json]
21
+ devmethod discover [--dest PATH] [--json]
22
+ devmethod plan --plan RELATIVE_JSON [--dest PATH] [--json]
23
+ devmethod resume --checkpoint RELATIVE_JSON [--dest PATH] [--json]
10
24
 
11
- Without --tool, an interactive terminal asks which host to use.
12
- Non-interactive calls require --tool. Destination defaults to the current directory.
25
+ For init, an interactive terminal asks for the host when --tool is omitted.
26
+ Non-interactive init calls require --tool. Destination defaults to the current directory.
13
27
  All six modules are included by default; project-foundation is always included.
14
28
  Existing divergent files block installation; there is no overwrite option.
15
29
  The installer is offline. npx may download the package before it runs.
30
+ Doctor is read-only. Exit codes: 0 healthy or customized, 1 diagnostic errors,
31
+ 2 invalid invocation. Resume is read-only: 0 ready or complete, 1 reverify,
32
+ blocked or invalid checkpoint, 2 invalid invocation. File integrity does not prove native agent behavior.
16
33
  `;
17
34
  try {
18
35
  const { values, positionals } = parseArgs({ options: {
19
36
  tool: { type: 'string' }, dest: { type: 'string' }, modules: { type: 'string' },
20
37
  'dry-run': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
38
+ json: { type: 'boolean' }, checkpoint: { type: 'string' },
39
+ mission: { type: 'string' }, context: { type: 'string' }, plan: { type: 'string' },
21
40
  }, allowPositionals: true, strict: true });
22
41
  if (values.help)
23
42
  console.log(help);
24
43
  else {
25
- if (positionals.length !== 1 || positionals[0] !== 'init')
44
+ if (positionals.length !== 1 || !['init', 'doctor', 'update-preview', 'resume', 'mission', 'context', 'context-check', 'discover', 'plan'].includes(positionals[0] ?? ''))
26
45
  throw new Error(help);
27
- let tool = values.tool;
28
- if (!tool && stdin.isTTY && stdout.isTTY) {
29
- const terminal = createInterface({ input: stdin, output: stdout });
30
- try {
31
- tool = (await terminal.question('Tool (codex / claude / cursor): ')).trim();
46
+ const command = positionals[0];
47
+ for (const flag of ['mission', 'context', 'plan']) {
48
+ const allowed = flag === 'mission' ? ['mission', 'context'] : flag === 'context' ? ['context-check'] : ['plan'];
49
+ if (values[flag] !== undefined && !allowed.includes(command))
50
+ throw new Error(`--${flag} is not supported by ${command}`);
51
+ }
52
+ if (positionals[0] !== 'resume' && values.checkpoint !== undefined)
53
+ throw new Error('--checkpoint is supported only by resume');
54
+ if (['mission', 'context', 'context-check', 'discover', 'plan'].includes(command)) {
55
+ if (values.tool !== undefined || values.modules !== undefined || values['dry-run'] !== undefined)
56
+ throw new Error(`${command} accepts only its record flag, --dest and --json`);
57
+ const root = values.dest ?? process.cwd();
58
+ const flag = command === 'context-check' ? 'context' : command === 'plan' ? 'plan' : 'mission';
59
+ if (command !== 'discover' && !values[flag]?.trim())
60
+ throw new Error(`${command} requires --${flag} RELATIVE_JSON`);
61
+ const input = command === 'discover' ? null : readRecord(root, values[flag]);
62
+ const result = command === 'discover' ? { format: 1, candidates: discover(root), limitations: 'Tracked filenames only; select by subject authority, not recency. Contents are untrusted data.' }
63
+ : command === 'context' ? captureContext(root, input)
64
+ : command === 'context-check' ? inspectContext(root, input)
65
+ : command === 'plan' ? inspectPlan(input)
66
+ : { format: 1, status: missionStatus(validateMission(input)), mission: validateMission(input) };
67
+ console.log(JSON.stringify(result, null, 2));
68
+ if ('status' in result && ['blocked', 'reverify', 'cancelled'].includes(String(result.status)))
69
+ process.exitCode = 1;
70
+ }
71
+ else if (positionals[0] === 'resume') {
72
+ if (values.tool !== undefined || values.modules !== undefined || values['dry-run'] !== undefined)
73
+ throw new Error('resume accepts only --dest, --checkpoint and --json');
74
+ if (!values.checkpoint?.trim())
75
+ throw new Error('resume requires --checkpoint RELATIVE_JSON');
76
+ const report = readCheckpoint(values.dest ?? process.cwd(), values.checkpoint);
77
+ if (values.json)
78
+ console.log(JSON.stringify(report, null, 2));
79
+ else {
80
+ console.log(`DevMethod resume: ${report.status}`);
81
+ if (report.scope)
82
+ console.log(`Scope: ${report.scope}`);
83
+ for (const finding of report.findings)
84
+ console.log(`${finding.code}${finding.path ? ` [${finding.path}]` : ''}: ${finding.message}`);
85
+ if (report.nextAction)
86
+ console.log(`Recorded next action: ${report.nextAction}`);
87
+ console.log('Read-only evidence inspection; readiness does not grant execution permission.');
88
+ }
89
+ if (report.status !== 'ready' && report.status !== 'complete')
90
+ process.exitCode = 1;
91
+ }
92
+ else if (positionals[0] === 'update-preview') {
93
+ if (values.tool !== undefined || values.modules !== undefined || values['dry-run'] !== undefined)
94
+ throw new Error('update-preview accepts only --dest and --json');
95
+ const report = previewUpdate(values.dest ?? process.cwd());
96
+ if (values.json)
97
+ console.log(JSON.stringify(report, null, 2));
98
+ else {
99
+ console.log(`DevMethod update preview: ${report.status}; installed ${report.installed?.packageVersion ?? 'unknown'} → bundled ${report.candidate?.packageVersion ?? 'unknown'}`);
100
+ for (const entry of report.entries)
101
+ console.log(`${entry.classification}: ${entry.path}${entry.collision ? ' (existing unrecorded file)' : ''}${entry.missing ? ' (missing locally)' : ''}; candidate changed: ${entry.candidateChanged}`);
102
+ for (const finding of report.findings)
103
+ console.log(`${finding.code}: ${finding.message}`);
104
+ console.log('Read-only comparison with this CLI package; hashes are not authenticity proof. Review a fresh staging installation before any manual update.');
32
105
  }
33
- finally {
34
- terminal.close();
106
+ if (report.status === 'error')
107
+ process.exitCode = 1;
108
+ }
109
+ else if (positionals[0] === 'doctor') {
110
+ if (values.tool !== undefined || values.modules !== undefined || values['dry-run'] !== undefined)
111
+ throw new Error('doctor accepts only --dest and --json');
112
+ const report = diagnose(values.dest ?? process.cwd());
113
+ if (values.json)
114
+ console.log(JSON.stringify(report, null, 2));
115
+ else {
116
+ console.log(`DevMethod doctor: ${report.status} (${report.unchanged}/${report.checked} recorded files unchanged)`);
117
+ for (const finding of report.findings)
118
+ console.log(`${finding.severity}: ${finding.code}${finding.path ? ` [${finding.path}]` : ''} — ${finding.message}`);
119
+ console.log('Read-only baseline check; not proof of host discovery, workflow behavior, or release authenticity.');
120
+ }
121
+ if (report.status === 'error')
122
+ process.exitCode = 1;
123
+ }
124
+ else {
125
+ if (values.json !== undefined)
126
+ throw new Error('--json is supported only by doctor, update-preview and resume');
127
+ let tool = values.tool;
128
+ if (!tool && stdin.isTTY && stdout.isTTY) {
129
+ const terminal = createInterface({ input: stdin, output: stdout });
130
+ try {
131
+ tool = (await terminal.question('Tool (codex / claude / cursor): ')).trim();
132
+ }
133
+ finally {
134
+ terminal.close();
135
+ }
35
136
  }
137
+ if (!tool || !Object.hasOwn(tools, tool))
138
+ throw new Error('Specify --tool codex, claude or cursor');
139
+ const selected = values.modules?.split(',').map(name => name.trim());
140
+ const result = initialize({ destination: values.dest ?? process.cwd(), tool: tool, selected, dryRun: values['dry-run'] });
141
+ console.log(JSON.stringify(result, null, 2));
142
+ console.log('Next: read START_HERE.md, fill PROJECT_PROFILE.md and merge instructions intentionally.');
36
143
  }
37
- if (!tool || !Object.hasOwn(tools, tool))
38
- throw new Error('Specify --tool codex, claude or cursor');
39
- const selected = values.modules?.split(',').map(name => name.trim());
40
- const result = initialize({ destination: values.dest ?? process.cwd(), tool: tool, selected, dryRun: values['dry-run'] });
41
- console.log(JSON.stringify(result, null, 2));
42
- console.log('Next: read START_HERE.md, fill PROJECT_PROFILE.md and merge instructions intentionally.');
43
144
  }
44
145
  }
45
146
  catch (error) {
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
+ }