devmethod-ai 0.1.0-rc.1 → 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 +34 -7
  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
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const arms = ['none', 'devmethod', 'bmad'];
5
+ const nonempty = value => typeof value === 'string' && value.trim().length > 0;
6
+ const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
7
+ /** Validate a complete matched batch, including blocked/failed runs. Never dispatch models. */
8
+ export function summarize(batch) {
9
+ if (!batch || batch.format !== 1 || !Array.isArray(batch.runs) || !batch.runs.length) throw new Error('Expected format 1 nonempty batch');
10
+ const { budget } = batch;
11
+ if (!budget || !Number.isSafeInteger(budget.runs) || budget.runs < 1 || !Number.isSafeInteger(budget.tokens) || budget.tokens < 1 || !finite(budget.costUSD) || !nonempty(budget.authorization)) throw new Error('Explicit run/token/cost budget and authorization required');
12
+ if (batch.runs.length > budget.runs) throw new Error('Run budget exceeded');
13
+ const groups = new Map();
14
+ const matchedKeys = ['fixtureDigest', 'promptDigest', 'hostVersion', 'model', 'toolsDigest', 'permissionsDigest', 'perRunTokens', 'perRunCostUSD', 'timeoutSeconds'];
15
+ const digests = ['fixtureDigest', 'promptDigest', 'toolsDigest', 'permissionsDigest'];
16
+ for (const run of batch.runs) {
17
+ if (!run || !nonempty(run.caseId) || !Number.isSafeInteger(run.repetition) || run.repetition < 1 || !arms.includes(run.arm) || !['passed', 'failed', 'blocked', 'timeout'].includes(run.status)) throw new Error('Invalid run identity or status');
18
+ if (!matchedKeys.every(key => ['perRunTokens', 'perRunCostUSD', 'timeoutSeconds'].includes(key) ? finite(run[key]) && (key === 'perRunCostUSD' || run[key] > 0) : nonempty(run[key])) || !digests.every(key => /^[a-f0-9]{64}$/.test(run[key]))) throw new Error('Missing pinned matched conditions');
19
+ if (!nonempty(run.methodRevision) || !nonempty(run.wrapper) || (run.arm === 'none' && run.methodRevision !== 'none')) throw new Error('Pin method revision and invocation wrapper');
20
+ if (!nonempty(run.evidence) || !nonempty(run.review)) throw new Error('Evidence reference and review or blocked reason required');
21
+ for (const metric of ['tokens', 'costUSD', 'elapsedSeconds']) if (run[metric] !== null && !finite(run[metric])) throw new Error('Usage must be measured nonnegative values or null (unavailable)');
22
+ if ((run.tokens !== null && run.tokens > run.perRunTokens) || (run.costUSD !== null && run.costUSD > run.perRunCostUSD)) throw new Error('Per-run budget exceeded');
23
+ const key = `${run.caseId}:${run.repetition}`;
24
+ const group = groups.get(key) ?? [];
25
+ if (group.some(other => other.arm === run.arm)) throw new Error('Duplicate arm');
26
+ if (group.length && matchedKeys.some(key => run[key] !== group[0][key])) throw new Error('Unmatched conditions');
27
+ group.push(run); groups.set(key, group);
28
+ }
29
+ for (const group of groups.values()) if (group.length !== 3) throw new Error('Each case/repetition requires all three arms, including blocked records');
30
+ for (const metric of ['tokens', 'costUSD']) if (batch.runs.reduce((sum, run) => sum + (run[metric] ?? 0), 0) > budget[metric]) throw new Error('Total budget exceeded');
31
+ const cells = [];
32
+ for (const caseId of new Set(batch.runs.map(run => run.caseId))) for (const arm of arms) {
33
+ const runs = batch.runs.filter(run => run.caseId === caseId && run.arm === arm);
34
+ cells.push({ caseId, arm, denominator: runs.length, ...Object.fromEntries(['passed', 'failed', 'blocked', 'timeout'].map(status => [status, runs.filter(run => run.status === status).length])) });
35
+ }
36
+ return { format: 1, cells, usageComplete: batch.runs.every(run => run.tokens !== null && run.costUSD !== null),
37
+ limitation: 'Validates record consistency only. Evidence authenticity and behavioral claims require independent inspection; no aggregate superiority score.' };
38
+ }
39
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
40
+ try { if (process.argv.length !== 3) throw new Error('Usage: node scripts/comparison.mjs BATCH_JSON'); console.log(JSON.stringify(summarize(JSON.parse(fs.readFileSync(process.argv[2]))), null, 2)); }
41
+ catch (error) { console.error(error.message); process.exitCode = 2; }
42
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { checkPath } from '../dist/filesystem.js';
7
+
8
+ const root = fileURLToPath(new URL('../', import.meta.url));
9
+ export const fixtures = JSON.parse(fs.readFileSync(path.join(root, 'evaluation/fixtures.json'))).cases;
10
+ const hash = data => createHash('sha256').update(data).digest('hex');
11
+ export function snapshot(directory) {
12
+ checkPath(path.resolve(directory));
13
+ const files = {};
14
+ function walk(dir, prefix = '') {
15
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a,b) => a.name.localeCompare(b.name))) {
16
+ if (entry.name === '.git') continue;
17
+ const relative = prefix + entry.name;
18
+ if (entry.isSymbolicLink()) throw new Error(`Symbolic evidence path: ${relative}`);
19
+ if (entry.isDirectory()) walk(path.join(dir, entry.name), relative + '/');
20
+ else if (entry.isFile()) files[relative] = hash(fs.readFileSync(path.join(dir, entry.name)));
21
+ else throw new Error(`Non-regular evidence path: ${relative}`);
22
+ }
23
+ }
24
+ walk(directory);
25
+ return files;
26
+ }
27
+ export function prepare(caseId, destination) {
28
+ const fixture = fixtures.find(item => item.id === caseId);
29
+ if (!fixture) throw new Error('Unknown case');
30
+ destination = path.resolve(destination);
31
+ checkPath(destination);
32
+ if (fs.existsSync(destination)) throw new Error('Use a fresh destination');
33
+ const source = path.join(root, fixture.directory);
34
+ if (JSON.stringify(Object.entries(snapshot(source)).sort()) !== JSON.stringify(Object.entries(fixture.files).sort())) throw new Error('Fixture differs from pinned manifest');
35
+ fs.cpSync(source, destination, { recursive: true, errorOnExist: true, force: false });
36
+ return { format: 1, caseId, destination, baseline: snapshot(destination), prompt: fixture.prompt };
37
+ }
38
+ /** Objective local checks only. This function never invokes or certifies a model. */
39
+ export function collect(caseId, destination, baseline, verifier) {
40
+ const fixture = fixtures.find(item => item.id === caseId);
41
+ if (!fixture) throw new Error('Unknown case');
42
+ const current = snapshot(destination);
43
+ for (const [name, digest] of Object.entries(fixture.files)) {
44
+ if (baseline[name] !== digest) throw new Error('Baseline does not contain the pinned fixture');
45
+ }
46
+ const changed = [...new Set([...Object.keys(baseline), ...Object.keys(current)])].filter(name => baseline[name] !== current[name]);
47
+ const unauthorizedChanges = changed.filter(name => !fixture.allowedChanges.includes(name));
48
+ const testsIntact = current['acceptance.test.mjs'] === fixture.files['acceptance.test.mjs'];
49
+ // Do not run replaced tests. A native evaluation must separately review model claims.
50
+ const environment = { ...process.env };
51
+ delete environment.NODE_TEST_CONTEXT;
52
+ const check = testsIntact ? verifier ? verifier(destination, fixture.check.slice(1)) : spawnSync(process.execPath, fixture.check.slice(1), { cwd: destination, env: environment, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024 }) : undefined;
53
+ return { format: 1, caseId, current, changed, unauthorizedChanges, testsIntact,
54
+ check: check ? { exit: check.status, signal: check.signal, stdout: check.stdout, stderr: check.stderr, error: check.error?.message } : { skipped: 'Acceptance tests changed' },
55
+ behavioralReview: 'pending', nativeHostEvidence: 'not established by this collector' };
56
+ }
57
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
58
+ try {
59
+ const [command, caseId, destination, baselineFile] = process.argv.slice(2);
60
+ if (!caseId || !destination || !['prepare', 'collect'].includes(command) || (command === 'collect' && !baselineFile)) throw new Error('Usage: node scripts/evaluation.mjs prepare CASE FRESH_DIR | collect CASE DIR BASELINE_JSON');
61
+ const result = command === 'prepare' ? prepare(caseId, destination) : collect(caseId, destination, JSON.parse(fs.readFileSync(baselineFile)).baseline);
62
+ console.log(JSON.stringify(result, null, 2));
63
+ } catch (error) { console.error(error.message); process.exitCode = 2; }
64
+ }
@@ -0,0 +1,30 @@
1
+ // Deterministic fixture walkthrough, not a native agent evaluation.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { captureContext } from '../dist/mission.js';
7
+ import { inspectCheckpoint } from '../dist/checkpoint.js';
8
+ import { digest } from '../dist/records.js';
9
+ const root = fileURLToPath(new URL('../examples/fullstack/', import.meta.url));
10
+ const mission = JSON.parse(fs.readFileSync(path.join(root, 'mission.json'), 'utf8'));
11
+ const before = captureContext(root, mission);
12
+ const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['test'], {
13
+ cwd: root, encoding: 'utf8', timeout: 120000, maxBuffer: 1024 * 1024,
14
+ env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' }
15
+ });
16
+ const log = `${result.stdout ?? ''}\n${result.stderr ?? ''}\nExit: ${result.status}; error: ${result.error?.message ?? 'none'}\n`;
17
+ fs.mkdirSync(path.join(root, 'evidence'), { recursive: true });
18
+ fs.writeFileSync(path.join(root, 'evidence/unit.log'), log);
19
+ const outcome = result.status === 0 ? 'passed' : 'failed';
20
+ const checkpoint = { format: 1, scope: 'AC-TITLE unit/HTTP/model verification only. AC-STORE and AC-WEB remain separate checks.',
21
+ status: 'active', nextAction: mission.nextAction, git: before.git,
22
+ sources: before.sources.map(({ id, path, sha256 }) => ({ id, path, sha256 })),
23
+ evidence: [{ id: 'unit', path: 'evidence/unit.log', sha256: digest(log), sourceIds: before.sources.map(s => s.id), dependsOn: [],
24
+ outcome, criterionIds: ['AC-TITLE'], kind: 'automated', revision: `${before.git.commit}; working-content ${before.git.diffSha256}` }] };
25
+ fs.writeFileSync(path.join(root, 'evidence/context.json'), JSON.stringify(before, null, 2));
26
+ fs.writeFileSync(path.join(root, 'evidence/checkpoint.json'), JSON.stringify(checkpoint, null, 2));
27
+ const report = inspectCheckpoint(root, checkpoint);
28
+ console.log(JSON.stringify({ kind: 'deterministic-fixture', command: 'npm test', outcome, checkpoint: 'examples/fullstack/evidence/checkpoint.json', report,
29
+ limitation: 'This invocation checks unit/HTTP/model only. Database, production e2e and native host behavior are separate evidence.' }, null, 2));
30
+ if (result.status !== 0 || report.status !== 'ready') process.exitCode = 1;
@@ -0,0 +1,13 @@
1
+ import path from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { checkPath } from '../../dist/filesystem.js';
4
+ /** macOS-only verified fixture runner. No network or writable filesystem. */
5
+ export function verifyFixture(directory, args) {
6
+ if (process.platform !== 'darwin') return { status:null, error:new Error('Native verification adapter currently supports macOS only.'),stdout:'',stderr:'' };
7
+ directory=path.resolve(directory);checkPath(directory);
8
+ const roots=[directory,path.dirname(process.execPath),'/System','/usr','/bin','/Library','/opt/homebrew','/dev'];
9
+ const profile=`(version 1) (allow default) (deny network*) (deny file-write*) (deny file-read* (subpath "/Users") (subpath "/private/tmp") (subpath "/private/var/folders")) (allow file-read-metadata) (allow file-read* ${roots.map(p=>`(subpath ${JSON.stringify(p)})`).join(' ')}) (allow file-write* (literal "/dev/null"))`;
10
+ return spawnSync('/usr/bin/sandbox-exec',['-p',profile,process.execPath,...args],{
11
+ cwd:directory,env:{PATH:process.env.PATH,LANG:'en_US.UTF-8'},encoding:'utf8',timeout:30000,maxBuffer:1024*1024
12
+ });
13
+ }
@@ -0,0 +1,17 @@
1
+ /** Reconcile a complete private app-server stream; cumulative totals are not deltas. */
2
+ export function meterCodexTree(events, root) {
3
+ const ids=new Set([root]), totals=new Map(), active=new Map(), ended=new Set();
4
+ for(const e of events) {
5
+ const p=e.params ?? {}, item=p.item;
6
+ if(item?.type==='subAgentActivity' && ids.has(p.threadId)) ids.add(item.agentThreadId);
7
+ if(e.method==='thread/started' && ids.has(p.thread?.parentThreadId))ids.add(p.thread.id);
8
+ if(e.method==='thread/tokenUsage/updated')totals.set(p.threadId,p.tokenUsage?.total);
9
+ if(e.method==='turn/started'){active.set(p.threadId,p.turn.id);ended.delete(p.threadId);}
10
+ if(e.method==='turn/completed'){active.delete(p.threadId);ended.add(p.threadId);}
11
+ }
12
+ const missing=[...ids].filter(id=>!id || !ended.has(id) || active.has(id) || !['inputTokens','outputTokens'].every(k=>Number.isSafeInteger(totals.get(id)?.[k]) && totals.get(id)[k]>=0));
13
+ // Usage for an undiscovered thread means attribution is incomplete.
14
+ for(const id of totals.keys())if(!ids.has(id))missing.push(id);
15
+ const usage=missing.length?null:{inputTokens:[...ids].reduce((n,id)=>n+totals.get(id).inputTokens,0),outputTokens:[...ids].reduce((n,id)=>n+totals.get(id).outputTokens,0),costUSD:null};
16
+ return {threads:[...ids],missing,usage};
17
+ }
@@ -0,0 +1,44 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { codexEnvironment } from './codex.mjs';
3
+
4
+ /** Private stdio server, bounded lifetime. Never attaches to the desktop daemon. */
5
+ export function startCodexRpc({ cwd, timeoutMs = 120000, onEvent = () => {}, command = 'codex', args = ['app-server', '--stdio', '--disable', 'apps', '--disable', 'plugins', '--disable', 'remote_plugin'] }) {
6
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000) throw new Error('Invalid RPC lifetime');
7
+ const child = spawn(command, args,
8
+ { cwd, env: codexEnvironment(), detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
9
+ const pending = new Map(); let next = 0, buffer = '', bytes = 0, stopped = false;
10
+ const kill = () => { try { process.platform === 'win32' ? child.kill('SIGKILL') : process.kill(-child.pid, 'SIGKILL'); } catch {} };
11
+ function close(error = new Error('RPC server closed')) {
12
+ if (stopped) return; stopped = true; clearTimeout(timer); process.removeListener('exit',kill); kill();
13
+ for (const { reject } of pending.values()) reject(error); pending.clear();
14
+ }
15
+ process.once('exit',kill);
16
+ const timer = setTimeout(() => close(new Error('RPC lifetime exceeded')), timeoutMs);
17
+ function send(message) { if (stopped) throw new Error('RPC closed'); child.stdin.write(JSON.stringify(message)+'\n'); }
18
+ child.stdout.setEncoding('utf8');
19
+ child.stdout.on('data', chunk => {
20
+ bytes += Buffer.byteLength(chunk); if (bytes > 2*1024*1024) return close(new Error('RPC output limit'));
21
+ buffer += chunk;
22
+ let end;
23
+ while ((end = buffer.indexOf('\n')) >= 0) {
24
+ const line = buffer.slice(0,end); buffer = buffer.slice(end+1); if (!line.trim()) continue;
25
+ try {
26
+ const message = JSON.parse(line);
27
+ if ('id' in message && !message.method) {
28
+ const call = pending.get(message.id); if (!call) continue; pending.delete(message.id);
29
+ message.error ? call.reject(new Error(JSON.stringify(message.error))) : call.resolve(message.result);
30
+ } else if ('id' in message) {
31
+ // Unattended probes never approve tool execution or fulfill external requests.
32
+ send({ id: message.id, error: { code: -32601, message: 'Client requests disabled in probe' } });
33
+ } else onEvent(message);
34
+ } catch (error) { close(error); }
35
+ }
36
+ });
37
+ child.stderr.on('data', chunk => { bytes += chunk.length; if (bytes > 2*1024*1024) close(new Error('RPC output limit')); });
38
+ child.on('error', close); child.on('close', () => close()); child.stdin.on('error', close);
39
+ return { close, notify: send, call(method, params = {}) {
40
+ if (stopped) return Promise.reject(new Error('RPC closed'));
41
+ const id = ++next;
42
+ return new Promise((resolve,reject) => { pending.set(id,{resolve,reject}); send({ id, method, params }); });
43
+ } };
44
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {startCodexRpc} from './codex-rpc.mjs';
4
+ import {meterCodexTree} from './codex-meter.mjs';
5
+ export async function runCodexTask({cwd,prompt,logFile,rpcFactory=startCodexRpc}) {
6
+ const events=[],turns=new Map(),drained=[];let root,finish;const done=new Promise(resolve=>{finish=resolve;});
7
+ const log=fs.openSync(logFile,'wx');
8
+ const rpc=rpcFactory({cwd,onEvent(e){
9
+ if(e.method==='account/rateLimits/updated')return;
10
+ events.push(e);fs.writeSync(log,JSON.stringify(e)+'\n');const p=e.params;
11
+ if(e.method==='turn/started')turns.set(p.threadId,p.turn.id);
12
+ if(e.method==='turn/completed'){turns.delete(p.threadId);if(p.threadId===root)finish(p.turn.status);if(!turns.size)drained.splice(0).forEach(resolve=>resolve());}
13
+ }});
14
+ let cancelled=false;const cancel=()=>{cancelled=true;finish('cancelled');};process.on('SIGINT',cancel);process.on('SIGTERM',cancel);
15
+ let status='failed',error,timer;
16
+ try{
17
+ await rpc.call('initialize',{clientInfo:{name:'devmethod_comparison',version:'0.1.0'},capabilities:{experimentalApi:true}});if(cancelled){status='cancelled';throw new Error('Cancelled before thread start');}rpc.notify({method:'initialized'});
18
+ const tmp=path.join(cwd,'.runtime/tmp');fs.mkdirSync(tmp,{recursive:true});
19
+ const start=await rpc.call('thread/start',{cwd,model:'gpt-5.6-sol',ephemeral:true,approvalPolicy:'never',sandbox:'workspace-write',config:{'model_reasoning_effort':'low','features.multi_agent':true,'agents.max_threads':4,'agents.max_depth':1,'web_search':'disabled','features.apps':false,'features.plugins':false,'features.remote_plugin':false,'sandbox_workspace_write.writable_roots':[cwd,path.join(cwd,'.git')],'sandbox_workspace_write.network_access':false,'sandbox_workspace_write.exclude_tmpdir_env_var':true,'sandbox_workspace_write.exclude_slash_tmp':true,'shell_environment_policy.inherit':'core','shell_environment_policy.set':{TMPDIR:tmp,UV_OFFLINE:'1',UV_PYTHON_DOWNLOADS:'never'}}});root=start.thread.id;if(cancelled){status='cancelled';throw new Error('Cancelled before model dispatch');}
20
+ await rpc.call('turn/start',{threadId:root,effort:'low',input:[{type:'text',text:prompt,text_elements:[]}]});
21
+ status=await Promise.race([done,new Promise(resolve=>{timer=setTimeout(()=>resolve('timeout'),110000);})]);
22
+ if(status==='completed'&&turns.size)status='incomplete';
23
+ if(status==='timeout'||status==='cancelled'||turns.size){
24
+ await Promise.allSettled([...turns].map(([threadId,turnId])=>rpc.call('turn/interrupt',{threadId,turnId})));
25
+ if(turns.size){let drainTimer;try{await Promise.race([new Promise(resolve=>drained.push(resolve)),new Promise(resolve=>{drainTimer=setTimeout(resolve,1500);})]);}finally{clearTimeout(drainTimer);}}
26
+ }
27
+ }catch(e){error=e.message;}finally{clearTimeout(timer);process.removeListener('SIGINT',cancel);process.removeListener('SIGTERM',cancel);rpc.close();fs.closeSync(log);}
28
+ return {status:status==='completed'?'exited':status,root,error,...meterCodexTree(events,root)};
29
+ }
@@ -0,0 +1,28 @@
1
+ /** Codex CLI 0.147.0 adapter. Optional tooling, never part of offline init. */
2
+ export const codexVersion = 'codex-cli 0.147.0';
3
+ export function codexArguments({ directory, model }) {
4
+ if (model !== 'gpt-5.6-sol') throw new Error('This pilot pins gpt-5.6-sol.');
5
+ return ['exec', '--ignore-user-config', '--ephemeral', '--json', '--color', 'never',
6
+ '--disable', 'apps', '--disable', 'plugins', '--disable', 'remote_plugin',
7
+ '--sandbox', 'workspace-write', '-C', directory, '--model', model,
8
+ '-c', 'model_reasoning_effort="low"', '-c', 'approval_policy="never"',
9
+ '-c', 'web_search="disabled"', '-c', 'sandbox_workspace_write.network_access=false',
10
+ '-c', 'sandbox_workspace_write.exclude_tmpdir_env_var=true',
11
+ '-c', 'sandbox_workspace_write.exclude_slash_tmp=true',
12
+ '-c', 'features.multi_agent=false', '-c', 'shell_environment_policy.inherit="core"',
13
+ '-c', 'shell_environment_policy.ignore_default_excludes=false', '-'];
14
+ }
15
+ export function codexUsage(lines) {
16
+ const turns = lines.filter(e => e.type === 'turn.completed');
17
+ if (!turns.length) return null;
18
+ const fields = ['input_tokens', 'output_tokens'];
19
+ if (turns.some(t => fields.some(k => !Number.isSafeInteger(t.usage?.[k]) || t.usage[k] < 0))) return null;
20
+ return { inputTokens: turns.reduce((n,t) => n+t.usage.input_tokens,0), outputTokens: turns.reduce((n,t) => n+t.usage.output_tokens,0),
21
+ cachedInputTokens: turns.every(t => Number.isSafeInteger(t.usage.cached_input_tokens)) ? turns.reduce((n,t) => n+t.usage.cached_input_tokens,0) : null,
22
+ costUSD: null };
23
+ }
24
+
25
+ export function codexEnvironment(source = process.env) {
26
+ const allowed = ['PATH','HOME','USER','LOGNAME','TMPDIR','TEMP','TMP','SystemRoot','SYSTEMROOT','WINDIR','COMSPEC','PATHEXT','LANG','LC_ALL','CODEX_HOME'];
27
+ return Object.fromEntries(allowed.filter(k => source[k] !== undefined).map(k => [k,source[k]]));
28
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { checkPath } from '../dist/filesystem.js';
5
+
6
+ /** Bounded process supervision. Success means process exit, not task acceptance. */
7
+ export async function supervise({ command, args, cwd, prompt, timeoutMs, signal, env, maxBytes = 2 * 1024 * 1024 }) {
8
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000) throw new Error('Timeout must be 1–120000 ms.');
9
+ if (signal?.aborted) return { status: 'cancelled', exit: null, stdout: '', stderr: '', elapsedSeconds: 0 };
10
+ const start = Date.now();
11
+ return await new Promise(resolve => {
12
+ const child = spawn(command, args, { cwd, env, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
13
+ let stdout = '', stderr = '', bytes = 0, stopped, killTimer, settled = false;
14
+ const kill = name => { try { if (process.platform !== 'win32') process.kill(-child.pid, name); else child.kill(name); } catch {} };
15
+ const stop = reason => { if (stopped) return; stopped = reason; kill('SIGTERM'); killTimer = setTimeout(() => kill('SIGKILL'), 1500); };
16
+ const timer = setTimeout(() => stop('timeout'), timeoutMs);
17
+ const cancel = () => stop('cancelled'); signal?.addEventListener('abort', cancel, { once: true });
18
+ const finish = (exit, error) => {
19
+ if (settled) return; settled = true; clearTimeout(timer); clearTimeout(killTimer); signal?.removeEventListener('abort', cancel);
20
+ // Terminate remaining descendants even if the direct child already exited.
21
+ if (process.platform !== 'win32') kill('SIGKILL');
22
+ resolve({ status: stopped ?? (error ? 'unavailable' : exit === 0 ? 'exited' : 'failed'), exit, error: error?.message,
23
+ stdout, stderr, elapsedSeconds: (Date.now()-start)/1000 });
24
+ };
25
+ const read = (chunk, kind) => { bytes += Buffer.byteLength(chunk); if (bytes > maxBytes) { stop('output-limit'); return; }
26
+ if (kind === 'out') stdout += chunk.toString(); else stderr += chunk.toString(); };
27
+ child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8');
28
+ child.stdout.on('data', c => read(c,'out')); child.stderr.on('data', c => read(c,'err'));
29
+ child.once('error', e => finish(null,e)); child.once('close', code => finish(code));
30
+ child.stdin.on('error', () => {}); child.stdin.end(prompt);
31
+ });
32
+ }
33
+ /** Immutable per-run slots avoid blind retries. Ledger lives outside worker writable roots. */
34
+ export function reserveRun(ledgerRoot, id, { maxRuns = 12, observedTokenStop = 500000 } = {}) {
35
+ if (!Number.isInteger(maxRuns) || maxRuns < 1 || maxRuns > 12 || !Number.isSafeInteger(observedTokenStop) || observedTokenStop < 1 || observedTokenStop > 500000) throw new Error('Invalid bounded pilot limits.');
36
+ if (!/^[a-zA-Z0-9-]{1,64}$/.test(id)) throw new Error('Invalid run ID.');
37
+ checkPath(path.resolve(ledgerRoot)); fs.mkdirSync(ledgerRoot, { recursive: true });
38
+ const lock = path.join(ledgerRoot, '.lock'); const fd = fs.openSync(lock, 'wx');
39
+ try {
40
+ const records = fs.readdirSync(ledgerRoot).filter(n=>n.endsWith('.json')).map(n=>JSON.parse(fs.readFileSync(path.join(ledgerRoot,n),'utf8')));
41
+ if (records.length >= maxRuns) throw new Error('Run limit reached.');
42
+ if (records.some(r=>!['exited','failed'].includes(r.status) || r.usage === null)) throw new Error('Interrupted, cancelled, unavailable or unmetered run requires reconciliation; no automatic retry.');
43
+ const used = records.reduce((n,r)=>n+r.usage.inputTokens+r.usage.outputTokens,0);
44
+ if (used >= observedTokenStop) throw new Error('Observed token stop reached. This is an inter-run stop, not a per-request cap.');
45
+ const file = path.join(ledgerRoot, `${id}.json`);
46
+ fs.writeFileSync(file, JSON.stringify({ id, status: 'running', usage: null }), { flag: 'wx' }); return file;
47
+ } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
48
+ }
49
+ export function recordRun(file, result) {
50
+ checkPath(path.resolve(file));
51
+ const previous = JSON.parse(fs.readFileSync(file));
52
+ if (previous.status !== 'running') throw new Error('Run already finalized.');
53
+ const temporary = `${file}.tmp`;
54
+ fs.writeFileSync(temporary, JSON.stringify({ ...result, id: previous.id }, null, 2), { flag: 'wx' }); fs.renameSync(temporary,file);
55
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import assert from 'node:assert/strict';
5
+ import { spawnSync } from 'node:child_process';
6
+ const archive = process.argv[2];
7
+ if (!archive) throw new Error('Usage: node scripts/package-smoke.mjs PACKAGE_TGZ');
8
+ const root = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'devmethod-package-'));
9
+ const run = (command, args, cwd = root, expected = 0, input) => {
10
+ const r = spawnSync(command, args, { cwd, encoding: 'utf8', timeout: 30000, input });
11
+ assert.equal(r.status, expected, `${command} ${args.join(' ')}\n${r.stdout}\n${r.stderr}`); return r.stdout;
12
+ };
13
+ try {
14
+ // stdin avoids GNU tar treating a Windows drive letter as a remote host.
15
+ run('tar', ['-xzf', '-'], root, 0, fs.readFileSync(path.resolve(archive)));
16
+ const pkg = path.join(root, 'package'); const cli = path.join(pkg, 'dist/cli.js');
17
+ const call = (args, expected = 0) => run(process.execPath, [cli, ...args], root, expected);
18
+ assert.equal(JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))).version, '0.1.0');
19
+ run(process.execPath, ['scripts/check-docs.mjs'], pkg);
20
+ for (const testFile of [...fs.readdirSync(path.join(pkg, 'examples/pocket-tasks/tests')).filter(f => f.endsWith('.test.mjs')).map(f => `examples/pocket-tasks/tests/${f}`), 'evaluation/greenfield/acceptance.test.mjs', 'evaluation/greenfield/security.test.mjs']) {
21
+ run(process.execPath, ['--test', testFile], pkg);
22
+ }
23
+ for (const host of ['codex', 'claude', 'cursor']) {
24
+ const project = path.join(root, host);
25
+ call(['init', '--tool', host, '--dest', project]);
26
+ assert.equal(JSON.parse(call(['doctor', '--dest', project, '--json'])).status, 'ok');
27
+ const profile = path.join(project, 'PROJECT_PROFILE.md'); fs.appendFileSync(profile, '\nFictional local customization.\n');
28
+ const before = fs.readFileSync(profile);
29
+ const preview = JSON.parse(call(['update-preview', '--dest', project, '--json']));
30
+ assert.equal(preview.entries.find(e => e.path === 'PROJECT_PROFILE.md').classification, 'customized');
31
+ call(['init', '--tool', host, '--dest', project], 2); assert.deepEqual(fs.readFileSync(profile), before);
32
+ }
33
+ call(['init', '--tool', 'claude', '--modules', 'scoped-delivery', '--dest', path.join(root, 'subset')]);
34
+ const project = path.join(root, 'mission'); fs.mkdirSync(path.join(project, 'examples'), { recursive: true });
35
+ fs.cpSync(path.join(pkg, 'examples/mission'), path.join(project, 'examples/mission'), { recursive: true });
36
+ fs.writeFileSync(path.join(project, '.gitignore'), 'evidence/\n');
37
+ run('git', ['init'], project); run('git', ['add', '.'], project);
38
+ run('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'fixture'], project);
39
+ const args = ['--dest', project, '--json'];
40
+ assert.equal(JSON.parse(call(['mission', '--mission', 'examples/mission/mission.json', ...args])).status, 'ready');
41
+ const context = call(['context', '--mission', 'examples/mission/mission.json', ...args]);
42
+ fs.mkdirSync(path.join(project, 'evidence')); fs.writeFileSync(path.join(project, 'evidence/context.json'), context);
43
+ assert.equal(JSON.parse(call(['context-check', '--context', 'evidence/context.json', ...args])).status, 'ready');
44
+ fs.appendFileSync(path.join(project, 'examples/mission/CONTRACT.md'), '\nChanged contract.\n');
45
+ assert.equal(JSON.parse(call(['context-check', '--context', 'evidence/context.json', ...args], 1)).status, 'reverify');
46
+ assert.deepEqual(JSON.parse(call(['plan', '--plan', 'examples/mission/plan.json', ...args])).candidates, ['api']);
47
+ if (process.argv[3]) {
48
+ const legacy = path.join(root, 'legacy-package'); fs.mkdirSync(legacy);
49
+ run('tar', ['-xzf', '-', '--strip-components=1'], legacy, 0, fs.readFileSync(path.resolve(process.argv[3])));
50
+ const adopted = path.join(root, 'legacy-adopted');
51
+ run(process.execPath, [path.join(legacy, 'dist/cli.js'), 'init', '--tool', 'codex', '--dest', adopted]);
52
+ const customized = path.join(adopted, '.agents/skills/project-foundation/SKILL.md');
53
+ const profile = path.join(adopted, 'PROJECT_PROFILE.md');
54
+ fs.appendFileSync(customized, '\nLocal workflow policy.\n'); fs.appendFileSync(profile, '\nFilled project context.\n');
55
+ const before = [fs.readFileSync(customized), fs.readFileSync(profile)];
56
+ const preview = JSON.parse(call(['update-preview', '--dest', adopted, '--json']));
57
+ assert.equal(preview.entries.find(e => e.path === '.agents/skills/project-foundation/SKILL.md').classification, 'conflict');
58
+ assert.equal(preview.provenance, 'unknown');
59
+ call(['init', '--tool', 'codex', '--dest', adopted], 2);
60
+ assert.deepEqual([fs.readFileSync(customized), fs.readFileSync(profile)], before);
61
+ console.log('Actual legacy tarball: local/upstream conflict detected and filled profile/custom skill preserved.');
62
+ }
63
+ console.log('Packed 0.1.0: three host installs, subset, customization preservation, mission/context/staleness/planning and documentation links passed. No native host execution.');
64
+ } finally { fs.rmSync(root, { recursive: true, force: true }); }
@@ -0,0 +1,41 @@
1
+ // One explicitly bounded infrastructure probe, not a comparative result.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { meterCodexTree } from './hosts/codex-meter.mjs';
5
+ import { startCodexRpc } from './hosts/codex-rpc.mjs';
6
+ import { reserveRun, recordRun } from './native-host.mjs';
7
+ const cancelling=process.argv.includes('--cancel');
8
+ const runId=cancelling?'children-cancel-preflight':'children-preflight';
9
+ const evidence=path.resolve('evaluation-private/comparison-v2'); fs.mkdirSync(evidence,{recursive:true});
10
+ const slot=reserveRun(path.join(evidence,'ledger'),runId);
11
+ const cwd=fs.mkdtempSync('/private/tmp/devmethod-child-probe-');
12
+ const log=fs.openSync(path.join(evidence,runId+'.jsonl'),'wx');
13
+ const events=[];const turns=new Map();let cancellation;
14
+ let root, done; const finished=new Promise(resolve=>{done=resolve;});
15
+ const totals=new Map(), children=new Set(), completed=new Set();
16
+ const rpc=startCodexRpc({cwd,onEvent(event){
17
+ events.push(event);
18
+ if(event.method==='account/rateLimits/updated')return;
19
+ fs.writeSync(log,JSON.stringify(event)+'\n'); const p=event.params;
20
+ if(p.item?.type==='subAgentActivity')children.add(p.item.agentThreadId);
21
+ if(event.method==='thread/started' && p.thread.parentThreadId)children.add(p.thread.id);
22
+ if(event.method==='thread/tokenUsage/updated')totals.set(p.threadId,p.tokenUsage.total);
23
+ if(event.method==='turn/started')turns.set(p.threadId,p.turn.id);
24
+ if(cancelling && !cancellation && event.method==='item/started' && p.item?.type==='commandExecution' && p.threadId!==root) {
25
+ cancellation=Promise.all([...turns].map(([threadId,turnId])=>rpc.call('turn/interrupt',{threadId,turnId})));
26
+ cancellation.catch(e=>done('interrupt-error: '+e.message));
27
+ }
28
+ if(event.method==='turn/completed') {completed.add(p.threadId); if(p.threadId===root)done(p.turn.status);}
29
+ }});
30
+ const abort=()=>{rpc.close();done('cancelled');};process.on('SIGINT',abort);process.on('SIGTERM',abort);
31
+ let status='failed',error;
32
+ try {
33
+ await rpc.call('initialize',{clientInfo:{name:'devmethod_probe',version:'0.1.0'},capabilities:{experimentalApi:true}});rpc.notify({method:'initialized'});
34
+ const result=await rpc.call('thread/start',{cwd,model:'gpt-5.6-sol',ephemeral:true,approvalPolicy:'never',sandbox:'read-only',config:{'model_reasoning_effort':'low','features.multi_agent':true,'agents.max_threads':2,'agents.max_depth':1,'features.apps':false,'features.plugins':false,'features.remote_plugin':false,'web_search':'disabled'},developerInstructions:'Infrastructure probe in a fictional empty directory. No file writes, network tools, external actions or purchases. Only the single requested child and its explicitly requested sleep command are authorized.'});
35
+ root=result.thread.id;
36
+ await rpc.call('turn/start',{threadId:root,effort:'low',input:[{type:'text',text:cancelling?'Spawn exactly one child to run the shell command sleep 30 and then answer CHILD_OK. Wait for the child and answer PARENT_OK. No other work or tools. This bounded delegation is authorized.':'Spawn exactly one child agent to answer CHILD_OK without using tools or spawning agents. Wait for that child to finish, then answer PARENT_OK. This bounded delegation is explicitly authorized.',text_elements:[]}]});
37
+ let deadline; try {status=await Promise.race([finished,new Promise((_,reject)=>{deadline=setTimeout(()=>reject(new Error('Probe deadline')),110000);})]);}finally{clearTimeout(deadline);}
38
+ if(cancelling && cancellation)await cancellation;
39
+ }catch(e){error=e.message;}finally{process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);rpc.close();fs.closeSync(log);}
40
+ const {usage}=meterCodexTree(events,root);
41
+ const record={status:status==='completed'||(cancelling && status==='interrupted')?'exited':'failed',probeStatus:status,cancellationRequested:Boolean(cancellation),usage,error,root,children:[...children],completed:[...completed],totals:Object.fromEntries(totals),note:'Observed per-thread totals; native accounting semantics still require review.'};recordRun(slot,record);console.log(JSON.stringify(record,null,2));
@@ -0,0 +1,45 @@
1
+ // Explicit bounded series. Source checkout and reviewed BMAD export required.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {execFileSync} from 'node:child_process';
5
+ import {spawnSync} from 'node:child_process';
6
+ import {codexEnvironment,codexVersion} from './hosts/codex.mjs';
7
+ import {createHash} from 'node:crypto';
8
+ import {initialize} from '../dist/init.js';
9
+ import {prepare,snapshot,collect} from './evaluation.mjs';
10
+ import {verifyFixture} from './hosts/checks.mjs';
11
+ import {runCodexTask} from './hosts/codex-task.mjs';
12
+ import {reserveRun,recordRun} from './native-host.mjs';
13
+ const count=Number(process.argv[2]), bmad=process.argv[3];
14
+ if(process.platform!=='darwin'||!Number.isInteger(count)||count<1||count>6||!bmad||!path.isAbsolute(bmad))throw Error('Usage: node scripts/run-comparison-v2.mjs COUNT_1_TO_6 ABSOLUTE_BMAD_EXPORT (macOS)');
15
+ if(execFileSync('codex',['--version'],{encoding:'utf8',env:codexEnvironment()}).trim()!==codexVersion)throw Error('Codex version mismatch');
16
+ const auth=spawnSync('codex',['login','status'],{encoding:'utf8',env:codexEnvironment(),timeout:10000});
17
+ if(auth.status!==0||!`${auth.stdout}${auth.stderr}`.includes('Logged in using ChatGPT'))throw Error('ChatGPT authentication required');
18
+ const evidence=path.resolve('evaluation-private/comparison-v2');fs.mkdirSync(evidence,{recursive:true});
19
+ const git=(cwd,args)=>execFileSync('git',['-C',cwd,...args],{encoding:'utf8'}).trim();
20
+ const revision=git(process.cwd(),['rev-parse','HEAD']);
21
+ const schedule=['bmad','devmethod','devmethod','bmad','bmad','devmethod'];
22
+ for(const [index,arm] of schedule.slice(0,count).entries()){
23
+ const id=`b1-${index+1}-${arm}`,file=path.join(evidence,'ledger',id+'.json');
24
+ if(fs.existsSync(file)){const previous=JSON.parse(fs.readFileSync(file));if(previous.status==='exited'&&previous.usage)continue;throw Error('Unsuccessful slot requires reconciliation');}
25
+ const slot=reserveRun(path.join(evidence,'ledger'),id);
26
+ const cwd=fs.mkdtempSync('/private/tmp/devmethod-comparison-'),fixture=path.join(cwd,'task');prepare('B1',fixture);
27
+ if(arm==='bmad')fs.cpSync(bmad,fixture,{recursive:true});else initialize({destination:fixture,tool:'codex'});
28
+ // Export rendering belongs to each invocation, not the preparation directory.
29
+ if(arm==='bmad')fs.rmSync(path.join(fixture,'_bmad/render'),{recursive:true,force:true});
30
+ git(fixture,['init','-b','fix-page-size']);
31
+ fs.writeFileSync(path.join(fixture,'.git/info/exclude'),'/.runtime/\n/_bmad/render/\n/_bmad-output/\n');
32
+ git(fixture,['add','.']);git(fixture,['-c','user.name=Evaluator','-c','user.email=evaluator@example.invalid','commit','-m','Pinned fictional baseline']);
33
+ const baseline=snapshot(fixture);
34
+ const prompt=(arm==='bmad'?'Use the installed bmad-build-auto skill. ':'Use the installed project-foundation skill and relevant references. ')+
35
+ 'Fix page-size.mjs so all supplied acceptance tests pass. Preserve acceptance.test.mjs and change no unrelated source files. You may create method artifacts under _bmad-output/, runtime renderings under _bmad/render/, and temporary files under .runtime/. Deliver the local fix and HANDOFF.md. Routine planning and implementation are authorized. Synchronous subagents are authorized, up to three child agents concurrently, depth one; no detached work. Git staging in this isolated repository is authorized; no commits, external writes, network tools, purchases or publication. Wait for all children before finishing.';
36
+ fs.writeFileSync(path.join(evidence,id+'-setup.json'),JSON.stringify({arm,revision,cwd:fixture,prompt,baseline,model:'gpt-5.6-sol',effort:'low',bmadVersion:'6.12.0',driverPins:Object.fromEntries(['run-comparison-v2.mjs','hosts/codex-task.mjs','hosts/codex-rpc.mjs','hosts/codex-meter.mjs','hosts/checks.mjs','hosts/codex.mjs','evaluation.mjs','native-host.mjs'].map(file=>[file,createHash('sha256').update(fs.readFileSync(new URL(file,import.meta.url))).digest('hex')]))},null,2));
37
+ console.log(JSON.stringify({event:'started',id}));
38
+ const result=await runCodexTask({cwd:fixture,prompt,logFile:path.join(evidence,id+'.jsonl')});
39
+ let checks;try{checks=collect('B1',fixture,baseline,verifyFixture);}catch(e){checks={error:e.message};}
40
+ const disallowed=checks.unauthorizedChanges?.filter(name=>baseline[name]!==undefined||!['_bmad-output/','_bmad/render/','.runtime/'].some(prefix=>name.startsWith(prefix)));
41
+ const handoffPresent=fs.existsSync(path.join(fixture,'HANDOFF.md'))&&fs.statSync(path.join(fixture,'HANDOFF.md')).isFile()&&fs.statSync(path.join(fixture,'HANDOFF.md')).size>0;
42
+ const acceptance=Boolean(handoffPresent&&checks.testsIntact&&checks.check.exit===0&&disallowed?.length===0);
43
+ recordRun(slot,{...result,arm,acceptance,handoffPresent,checks,disallowed});console.log(JSON.stringify({event:'finished',id,status:result.status,usage:result.usage,acceptance}));
44
+ if(result.status!=='exited'||!result.usage)break;
45
+ }
@@ -0,0 +1,76 @@
1
+ // Explicit opt-in fixture campaign. Usage: node scripts/run-native-pilot.mjs ABSOLUTE_ROOT COUNT
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createHash } from 'node:crypto';
7
+ import { fixtures, prepare, snapshot, collect } from './evaluation.mjs';
8
+ import { verifyFixture } from './hosts/checks.mjs';
9
+ import { initialize } from '../dist/init.js';
10
+ import { supervise, reserveRun, recordRun } from './native-host.mjs';
11
+ import { codexArguments, codexEnvironment, codexVersion, codexUsage } from './hosts/codex.mjs';
12
+ const source=fileURLToPath(new URL('../',import.meta.url));
13
+ const root=process.argv[2]; const count=Number(process.argv[3]);
14
+ if (!root || !path.isAbsolute(root) || !Number.isInteger(count) || count<1 || count>12) throw new Error('Supply absolute disposable campaign root and 1–12 slots.');
15
+ if (process.platform !== 'darwin') throw new Error('This native fixture pilot is validated on macOS only.');
16
+ const env=codexEnvironment();
17
+ const probe=args=>execFileSync('codex',args,{encoding:'utf8',env,timeout:10000,stdio:['ignore','pipe','pipe']}).trim();
18
+ if (probe(['--version'])!==codexVersion) throw new Error('Installed Codex version differs from pinned pilot.');
19
+ // login status may write its human-readable result to stderr.
20
+ const {spawnSync}=await import('node:child_process');
21
+ const auth=spawnSync('codex',['login','status'],{encoding:'utf8',env,timeout:10000});
22
+ if(auth.status!==0 || !`${auth.stdout}${auth.stderr}`.includes('Logged in using ChatGPT')) throw new Error('ChatGPT CLI authentication required; no API fallback.');
23
+ fs.mkdirSync(root,{recursive:true});
24
+ const ledger=path.join(root,'ledger'), evidence=path.join(root,'evidence'), worktrees=path.join(root,'worktrees'), base=path.join(root,'base');
25
+ for(const dir of [evidence,worktrees]) fs.mkdirSync(dir,{recursive:true});
26
+ const git=(dir,args)=>execFileSync('git',['-c','core.fsmonitor=false','-C',dir,...args],{encoding:'utf8',timeout:10000,stdio:['ignore','pipe','pipe']}).trim();
27
+ if(!fs.existsSync(base)) {fs.mkdirSync(base);git(base,['init']);fs.writeFileSync(path.join(base,'WORKSPACE.md'),'Fictional evaluation workspace. Local task scope only. No external actions.\n');git(base,['add','.']);git(base,['-c','user.name=Evaluator','-c','user.email=evaluator@example.invalid','commit','-m','Fictional baseline']);}
28
+ const hash=value=>createHash('sha256').update(value).digest('hex');
29
+ const revision=git(source,['rev-parse','HEAD']);
30
+ const schedule=[{id:'b1-1-none',caseId:'B1',arm:'none',repetition:1,purpose:'calibration-only'}];
31
+ for(let rep=1;rep<=3;rep++) for(const arm of [['none','devmethod','bmad'],['devmethod','bmad','none'],['bmad','none','devmethod']][rep-1]) schedule.push({id:`matched-b1-${rep}-${arm}`,caseId:'B1',arm,repetition:rep});
32
+ for(const caseId of ['B3','B4'])schedule.push({id:`smoke-${caseId.toLowerCase()}`,caseId,arm:'devmethod',repetition:1});
33
+ // Prioritize complete three-arm coverage and failure/resumption before repetitions.
34
+ const probes=schedule.splice(10,2);schedule.splice(4,0,...probes);
35
+ const controller=new AbortController(); const stop=()=>controller.abort();process.on('SIGINT',stop);process.on('SIGTERM',stop);
36
+ try {
37
+ for(const entry of schedule.slice(0,count)) {
38
+ const prior=path.join(ledger,entry.id+'.json');
39
+ if(fs.existsSync(prior)) {
40
+ const record=JSON.parse(fs.readFileSync(prior));
41
+ if(record.status==='exited' && record.usage)continue;
42
+ throw new Error('Existing unsuccessful/interrupted slot requires explicit reconciliation; no automatic retry.');
43
+ }
44
+ if(controller.signal.aborted)break;
45
+ // Reserve before setup; a setup failure leaves a slot requiring reconciliation.
46
+ const slot=reserveRun(ledger,entry.id);
47
+ const dir=path.join(worktrees,entry.id), staging=path.join(root,'staging-'+entry.id);
48
+ git(base,['worktree','add','-b',entry.id,dir]);
49
+ const prepared=prepare(entry.caseId,staging);fs.cpSync(staging,dir,{recursive:true});fs.rmSync(staging,{recursive:true});
50
+ let wrapper='';
51
+ if(entry.arm==='devmethod') {initialize({destination:dir,tool:'codex'});wrapper='Use the installed project-foundation skill and only its relevant references. ';}
52
+ if(entry.arm==='bmad') {
53
+ const bmad=path.join(root,'bmad-export');
54
+ if(!fs.existsSync(path.join(bmad,'.agents/skills')))throw new Error('Place the reviewed pinned BMAD 6.12.0 export in campaign-root/bmad-export before admission.');
55
+ fs.cpSync(bmad,dir,{recursive:true});wrapper='Use the installed BMAD method and the relevant skill for this bounded task. ';
56
+ }
57
+ const baseline=snapshot(dir); git(dir,['add','.']);git(dir,['-c','user.name=Evaluator','-c','user.email=evaluator@example.invalid','commit','-m','Pinned arm setup']);
58
+ const fixture=fixtures.find(f=>f.id===entry.caseId);
59
+ const prompt=wrapper+(entry.caseId==='B4'?'Run the initial tests and explicitly report their real failure before correcting and resuming. ':'')+fixture.prompt+'\nRespond in English. Stay in this worktree. No network, purchases, external writes, nested agents, commits or publication. Record a concise HANDOFF.md. Stop at the stated task scope.';
60
+ const args=codexArguments({directory:dir,model:'gpt-5.6-sol'});
61
+ const meta={...entry,driverPins:Object.fromEntries(['scripts/run-native-pilot.mjs','scripts/native-host.mjs','scripts/hosts/codex.mjs','scripts/hosts/checks.mjs'].map(p=>[p,hash(fs.readFileSync(path.join(source,p)))])),methodRevision:entry.arm==='none'?'none':entry.arm==='bmad'?'bmad-method@6.12.0':revision,hostVersion:codexVersion,model:'gpt-5.6-sol',effort:'low',prompt,fixtureDigest:hash(JSON.stringify(fixture)),promptDigest:hash(fixture.prompt),baseline,args,timeoutSeconds:120};
62
+ fs.writeFileSync(path.join(evidence,entry.id+'-setup.json'),JSON.stringify(meta,null,2));
63
+ console.log(JSON.stringify({event:'started',id:entry.id}));
64
+ const result=await supervise({command:'codex',args,cwd:dir,prompt,timeoutMs:120000,signal:controller.signal,env});
65
+ fs.writeFileSync(path.join(evidence,entry.id+'.jsonl'),result.stdout);fs.writeFileSync(path.join(evidence,entry.id+'.stderr'),result.stderr);
66
+ let events;try {events=result.stdout.trim().split('\n').filter(Boolean).map(line=>JSON.parse(line));}catch{events=[];}
67
+ const usage=codexUsage(events);let checks;
68
+ try { checks=collect(entry.caseId,dir,baseline,verifyFixture); } catch(error) { checks={error:error.message,testsIntact:false}; }
69
+ const acceptance=Boolean(checks.testsIntact && checks.unauthorizedChanges.length===0 && checks.check.exit===(entry.caseId==='B5'?1:0));
70
+ const record={...entry,status:result.status,exit:result.exit,error:result.error,usage,elapsedSeconds:result.elapsedSeconds,acceptance,checks,
71
+ transcriptSha256:hash(result.stdout),review:'pending; process exit and objective checks do not certify instruction adherence'};
72
+ recordRun(slot,record);
73
+ console.log(JSON.stringify({event:'finished',id:entry.id,status:result.status,acceptance,usage,elapsedSeconds:result.elapsedSeconds}));
74
+ if(result.status!=='exited'||!usage)break;
75
+ }
76
+ }finally{process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);}
@@ -0,0 +1,7 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const root=process.argv[2];if(!root)throw new Error('Usage: node scripts/summarize-native-pilot.mjs CAMPAIGN_ROOT');
4
+ const runs=fs.readdirSync(path.join(root,'ledger')).filter(n=>n.endsWith('.json')).sort().map(n=>JSON.parse(fs.readFileSync(path.join(root,'ledger',n))));
5
+ const totalTokens=runs.every(r=>r.usage)?runs.reduce((n,r)=>n+r.usage.inputTokens+r.usage.outputTokens,0):null;
6
+ const cells=['none','devmethod','bmad'].map(arm=>{const rs=runs.filter(r=>r.id.startsWith('matched-')&&r.arm===arm);return {arm,planned:3,executed:rs.length,notRun:3-rs.length,objectivePassed:rs.filter(r=>r.acceptance===true).length,objectiveFailed:rs.filter(r=>r.acceptance===false).length};});
7
+ console.log(JSON.stringify({format:1,kind:'bounded-native-pilot',runs:runs.map(({id,status,acceptance,usage,elapsedSeconds,review})=>({id,status,objectiveChecksPassed:acceptance,usage,elapsedSeconds,review})),cells,totalTokens,costUSD:null,limits:{maxRuns:12,timeoutSeconds:120,observedTokenStop:500000},limitation:'Calibration excluded from matched cells. Objective success is not behavioral review or proof of superiority. Unknown cost remains unavailable.'},null,2));