musubix3 0.1.19 → 0.1.21

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 (80) hide show
  1. package/.github/plugin/marketplace.json +2 -2
  2. package/.github/skills/sdd-change/SKILL.md +28 -45
  3. package/CHANGELOG.md +126 -0
  4. package/README-ja.md +171 -5
  5. package/README.md +185 -8
  6. package/dist/packages/analysis/src/adapters.d.ts +2 -2
  7. package/dist/packages/analysis/src/adapters.js +33 -3
  8. package/dist/packages/analysis/src/adapters.js.map +1 -1
  9. package/dist/packages/analysis/src/approval-record.js +4 -0
  10. package/dist/packages/analysis/src/approval-record.js.map +1 -1
  11. package/dist/packages/analysis/src/approval.d.ts +51 -0
  12. package/dist/packages/analysis/src/approval.js +341 -2
  13. package/dist/packages/analysis/src/approval.js.map +1 -1
  14. package/dist/packages/analysis/src/attestation.js +4 -0
  15. package/dist/packages/analysis/src/attestation.js.map +1 -1
  16. package/dist/packages/analysis/src/change-evidence.d.ts +65 -8
  17. package/dist/packages/analysis/src/change-evidence.js +173 -46
  18. package/dist/packages/analysis/src/change-evidence.js.map +1 -1
  19. package/dist/packages/analysis/src/change-waiver.d.ts +46 -13
  20. package/dist/packages/analysis/src/change-waiver.js +481 -152
  21. package/dist/packages/analysis/src/change-waiver.js.map +1 -1
  22. package/dist/packages/analysis/src/change.d.ts +1 -0
  23. package/dist/packages/analysis/src/change.js +194 -66
  24. package/dist/packages/analysis/src/change.js.map +1 -1
  25. package/dist/packages/analysis/src/evidence-merge-guard.d.ts +9 -0
  26. package/dist/packages/analysis/src/evidence-merge-guard.js +62 -0
  27. package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -0
  28. package/dist/packages/analysis/src/evidence-merge.d.ts +55 -0
  29. package/dist/packages/analysis/src/evidence-merge.js +1182 -0
  30. package/dist/packages/analysis/src/evidence-merge.js.map +1 -0
  31. package/dist/packages/analysis/src/evidence-writer-lock.d.ts +189 -0
  32. package/dist/packages/analysis/src/evidence-writer-lock.js +843 -0
  33. package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -0
  34. package/dist/packages/analysis/src/files.js +11 -0
  35. package/dist/packages/analysis/src/files.js.map +1 -1
  36. package/dist/packages/analysis/src/formal.js +7 -0
  37. package/dist/packages/analysis/src/formal.js.map +1 -1
  38. package/dist/packages/analysis/src/gate.d.ts +1 -1
  39. package/dist/packages/analysis/src/gate.js +23 -6
  40. package/dist/packages/analysis/src/gate.js.map +1 -1
  41. package/dist/packages/analysis/src/graph.js +6 -0
  42. package/dist/packages/analysis/src/graph.js.map +1 -1
  43. package/dist/packages/analysis/src/index.d.ts +3 -0
  44. package/dist/packages/analysis/src/index.js +3 -0
  45. package/dist/packages/analysis/src/index.js.map +1 -1
  46. package/dist/packages/analysis/src/knowledge.js +4 -0
  47. package/dist/packages/analysis/src/knowledge.js.map +1 -1
  48. package/dist/packages/analysis/src/model-correspondence.js +5 -1
  49. package/dist/packages/analysis/src/model-correspondence.js.map +1 -1
  50. package/dist/packages/analysis/src/mutation.js +4 -0
  51. package/dist/packages/analysis/src/mutation.js.map +1 -1
  52. package/dist/packages/analysis/src/order.d.ts +16 -1
  53. package/dist/packages/analysis/src/order.js +10 -0
  54. package/dist/packages/analysis/src/order.js.map +1 -1
  55. package/dist/packages/analysis/src/performance.js +5 -1
  56. package/dist/packages/analysis/src/performance.js.map +1 -1
  57. package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
  58. package/dist/packages/analysis/src/quality-refresh.js +312 -0
  59. package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
  60. package/dist/packages/analysis/src/tdd.d.ts +86 -3
  61. package/dist/packages/analysis/src/tdd.js +604 -52
  62. package/dist/packages/analysis/src/tdd.js.map +1 -1
  63. package/dist/packages/analysis/src/trace.d.ts +24 -0
  64. package/dist/packages/analysis/src/trace.js +101 -3
  65. package/dist/packages/analysis/src/trace.js.map +1 -1
  66. package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
  67. package/dist/packages/analysis/src/workflow-waiver.js +9 -5
  68. package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
  69. package/dist/packages/analysis/src/workflow.d.ts +1 -1
  70. package/dist/packages/analysis/src/workflow.js +103 -21
  71. package/dist/packages/analysis/src/workflow.js.map +1 -1
  72. package/dist/packages/cli/src/install.js +51 -6
  73. package/dist/packages/cli/src/install.js.map +1 -1
  74. package/dist/packages/cli/src/main.js +252 -79
  75. package/dist/packages/cli/src/main.js.map +1 -1
  76. package/dist/packages/domain/src/design.js +8 -1
  77. package/dist/packages/domain/src/design.js.map +1 -1
  78. package/dist/packages/domain/src/types.d.ts +1 -0
  79. package/package.json +7 -2
  80. package/plugin.json +1 -1
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError, InvalidArgumentError } from 'commander';
3
+ import { realpathSync } from 'node:fs';
3
4
  import { basename, relative, resolve } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { c4Diagram, validateConstitution, validateDesign, validateRequirements, } from '../../domain/src/index.js';
6
- import { buildKnowledge, buildTrace, changedFiles, checkTrace, configLint, cycles, exists, files, formalCheck, graphGate, graphImpact, indexGraph, loadConfig, loadGraph, loadTrace, portable, projectStatus, queryKnowledge, formalDoctor, generateFormalArtifacts, readText, runGate, traceImpact, changePhases, recordChangePhase, recordWorkflow, runTddPhase, sanitizeWorkflowLogFile, validateTddEvidence, verifyWorkflowLogFile, migrateTddFingerprint, voidTddCycle, attestationSigningPayload, createUnsignedAttestation, githubOidcAudience, verifyEvidenceAttestation, mutationDoctor, mutationIdentity, validateMutationEvidence, validateModelCorrespondenceEvidence, within, approvalManifest, approvalStages, recordApproval, requireApproval, requireDomainOption, requireValidateDomainOption, resolveDesignFileDomain, resolveNamedDomain, validateApprovals, validateApprovalsForDomain, scaffoldCommands, scaffoldRequirements, scaffoldDesign, recordChangeWaiver, recordWorkflowWaiver, recordAllWorkflowWaivers, } from '../../analysis/src/index.js';
7
+ import { buildKnowledge, buildTrace, changedFiles, checkTrace, configLint, cycles, exists, files, formalCheck, graphGate, graphImpact, indexGraph, loadConfig, loadGraph, loadTrace, portable, projectStatus, queryKnowledge, formalDoctor, generateFormalArtifacts, readText, runGate, traceImpact, changePhases, recordChangePhase, recordWorkflow, runTddPhase, sanitizeWorkflowLogFile, validateTddEvidence, verifyWorkflowLogFile, migrateTddFingerprint, migrateTddIdentifier, voidTddCycle, archiveTddCycle, mergeEvidenceHistories, recoverEvidenceMerge, recoverQualityRefresh, attestationSigningPayload, createUnsignedAttestation, githubOidcAudience, verifyEvidenceAttestation, mutationDoctor, mutationIdentity, validateMutationEvidence, validateModelCorrespondenceEvidence, within, approvalManifest, approvalStages, diffOnlyChangedFiles, loadApproval, recordApproval, requireApproval, requireDomainOption, requireValidateDomainOption, resolveDesignFileDomain, resolveNamedDomain, validateApprovals, validateApprovalsForDomain, scaffoldCommands, scaffoldRequirements, scaffoldDesign, recordChangeWaiver, recordWorkflowWaiver, recordAllWorkflowWaivers, assertCoordinatedEvidenceRead, EvidenceProtectedOutputError, EvidenceWriterLockError, recoverEvidenceWriterLock, withEvidenceWriterLock, } from '../../analysis/src/index.js';
7
8
  import { install, pluginInstall, upgradeSkills } from './install.js';
8
9
  const packageRoot = fileURLToPath(new URL('../../../../', import.meta.url));
9
10
  function output(value, json, summary) {
@@ -23,8 +24,12 @@ function common(command) {
23
24
  function pathQuery(root, query) {
24
25
  return portable(relative(root, resolve(root, query)));
25
26
  }
27
+ async function coordinatedReader(root, operation) {
28
+ await assertCoordinatedEvidenceRead(root);
29
+ return operation();
30
+ }
26
31
  export function createProgram() {
27
- const program = new Command().name('musubix3').description('Evidence-driven SDD for GitHub Copilot CLI / 根拠に基づく仕様駆動開発').version('0.1.19');
32
+ const program = new Command().name('musubix3').description('Evidence-driven SDD for GitHub Copilot CLI / 根拠に基づく仕様駆動開発').version('0.1.21');
28
33
  program.exitOverride();
29
34
  common(program.command('init').alias('install').description('Install repository skills and SDD artifacts (preserves existing files)'))
30
35
  .option('--dry-run', 'Preview without writing').option('--force', 'Replace bundled, managed paths only')
@@ -85,27 +90,31 @@ export function createProgram() {
85
90
  }
86
91
  common(design.command('validate <file>')).action(async (file, options) => {
87
92
  const root = resolve(options.root);
88
- if (await exists(within(root, '.musubix/config.json'))) {
89
- const config = await loadConfig(root);
90
- const domain = await resolveDesignFileDomain(root, config.approval, portable(relative(root, resolve(root, file))));
91
- await requireApproval(root, 'requirements', config.approval, domain);
92
- }
93
- result(await designResult(file, root), !!options.json);
93
+ await coordinatedReader(root, async () => {
94
+ if (await exists(within(root, '.musubix/config.json'))) {
95
+ const config = await loadConfig(root);
96
+ const domain = await resolveDesignFileDomain(root, config.approval, portable(relative(root, resolve(root, file))));
97
+ await requireApproval(root, 'requirements', config.approval, domain);
98
+ }
99
+ result(await designResult(file, root), !!options.json);
100
+ });
94
101
  });
95
102
  common(design.command('c4 <file>')).action(async (file, options) => {
96
103
  const root = resolve(options.root);
97
- if (await exists(within(root, '.musubix/config.json'))) {
98
- const config = await loadConfig(root);
99
- const domain = await resolveDesignFileDomain(root, config.approval, portable(relative(root, resolve(root, file))));
100
- await requireApproval(root, 'requirements', config.approval, domain);
101
- }
102
- const report = await designResult(file, root);
103
- if (!report.valid) {
104
- result(report, !!options.json);
105
- return;
106
- }
107
- const diagram = c4Diagram(report.value);
108
- output({ diagram }, !!options.json, diagram);
104
+ await coordinatedReader(root, async () => {
105
+ if (await exists(within(root, '.musubix/config.json'))) {
106
+ const config = await loadConfig(root);
107
+ const domain = await resolveDesignFileDomain(root, config.approval, portable(relative(root, resolve(root, file))));
108
+ await requireApproval(root, 'requirements', config.approval, domain);
109
+ }
110
+ const report = await designResult(file, root);
111
+ if (!report.valid) {
112
+ result(report, !!options.json);
113
+ return;
114
+ }
115
+ const diagram = c4Diagram(report.value);
116
+ output({ diagram }, !!options.json, diagram);
117
+ });
109
118
  });
110
119
  common(design.command('scaffold <slug>')).action(async (slug, options) => {
111
120
  const path = await scaffoldDesign(resolve(options.root), slug);
@@ -121,10 +130,12 @@ export function createProgram() {
121
130
  common(trace.command('check')).option('--strict', 'Fail missing mandatory coverage')
122
131
  .action(async (options) => {
123
132
  const root = resolve(options.root);
133
+ await assertCoordinatedEvidenceRead(root);
124
134
  result(await checkTrace(root, await loadTrace(root), !!options.strict), !!options.json);
125
135
  });
126
136
  common(trace.command('impact <id-or-path>')).action(async (query, options) => {
127
137
  const root = resolve(options.root);
138
+ await assertCoordinatedEvidenceRead(root);
128
139
  const graph = await loadTrace(root);
129
140
  const freshness = await checkTrace(root, graph);
130
141
  const stale = freshness.diagnostics.some((d) => d.code.startsWith('TRACE_STALE'));
@@ -144,28 +155,35 @@ export function createProgram() {
144
155
  common(graph.command('index')).option('--changed', 'Report changed files; conservatively refresh full graph')
145
156
  .action(async (options) => {
146
157
  const root = resolve(options.root);
147
- const changed = options.changed ? await changedFiles(root) : null;
148
- const indexed = await indexGraph(root);
149
- output({ ...indexed, changed }, !!options.json, `Graph: ${indexed.files.length} files, ${indexed.imports.length} imports, ${indexed.symbols.length} symbols.`);
150
- if (indexed.diagnostics.some((d) => d.severity === 'error'))
151
- process.exitCode = 1;
158
+ await withEvidenceWriterLock(root, 'graph index', async () => {
159
+ const changed = options.changed ? await changedFiles(root) : null;
160
+ const indexed = await indexGraph(root);
161
+ output({ ...indexed, changed }, !!options.json, `Graph: ${indexed.files.length} files, ${indexed.imports.length} imports, ${indexed.symbols.length} symbols.`);
162
+ if (indexed.diagnostics.some((d) => d.severity === 'error'))
163
+ process.exitCode = 1;
164
+ });
152
165
  });
153
166
  common(graph.command('impact <symbol-or-path>')).action(async (query, options) => {
154
167
  const root = resolve(options.root);
168
+ await assertCoordinatedEvidenceRead(root);
155
169
  const indexed = await loadGraph(root);
156
170
  const normalized = indexed.files.includes(pathQuery(root, query)) ? pathQuery(root, query) : query;
157
171
  output(graphImpact(indexed, normalized), !!options.json);
158
172
  });
159
173
  common(graph.command('cycles')).action(async (options) => {
160
- const found = cycles(await loadGraph(resolve(options.root)));
174
+ const root = resolve(options.root);
175
+ await assertCoordinatedEvidenceRead(root);
176
+ const found = cycles(await loadGraph(root));
161
177
  output({ cycles: found }, !!options.json);
162
178
  if (found.length)
163
179
  process.exitCode = 1;
164
180
  });
165
181
  common(graph.command('gate')).action(async (options) => {
166
182
  const root = resolve(options.root);
167
- const config = await loadConfig(root);
168
- result(graphGate(await indexGraph(root), config.architecture, config.codeGraph), !!options.json);
183
+ await withEvidenceWriterLock(root, 'graph gate', async () => {
184
+ const config = await loadConfig(root);
185
+ result(graphGate(await indexGraph(root), config.architecture, config.codeGraph), !!options.json);
186
+ });
169
187
  });
170
188
  const knowledge = program.command('knowledge').description('Local artifact and Git evidence retrieval (TF-IDF, not GraphRAG)');
171
189
  common(knowledge.command('build')).action(async (options) => {
@@ -177,7 +195,9 @@ export function createProgram() {
177
195
  const limit = Number(options.limit);
178
196
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
179
197
  throw new Error('--limit must be 1..100.');
180
- output(await queryKnowledge(resolve(options.root), query.join(' '), limit), !!options.json);
198
+ const root = resolve(options.root);
199
+ await assertCoordinatedEvidenceRead(root);
200
+ output(await queryKnowledge(root, query.join(' '), limit), !!options.json);
181
201
  });
182
202
  const formal = program.command('formal').description('Honest consistency checking of an explicit abstraction');
183
203
  common(formal.command('check <file>'))
@@ -192,12 +212,12 @@ export function createProgram() {
192
212
  if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 300_000)
193
213
  throw new Error('--timeout must be 100..300000 milliseconds.');
194
214
  const root = resolve(options.root);
195
- const report = await formalCheck(await readText(root, file), root, {
215
+ const report = await withEvidenceWriterLock(root, 'formal check', async () => formalCheck(await readText(root, file), root, {
196
216
  solver: options.solver,
197
217
  timeoutMs,
198
218
  ...(options.z3Command ? { z3Command: options.z3Command } : {}),
199
219
  ...(options.leanCommand ? { leanCommand: options.leanCommand } : {}),
200
- });
220
+ }));
201
221
  output(report, !!options.json);
202
222
  if (!report.valid)
203
223
  process.exitCode = 1;
@@ -209,7 +229,7 @@ export function createProgram() {
209
229
  throw new Error('Unknown format; use both, smt2, or lean.');
210
230
  const root = resolve(options.root);
211
231
  const formats = options.format === 'both' ? ['smt2', 'lean'] : [options.format];
212
- const report = await generateFormalArtifacts(await readText(root, file), root, [...formats]);
232
+ const report = await withEvidenceWriterLock(root, 'formal generate', async () => generateFormalArtifacts(await readText(root, file), root, [...formats]));
213
233
  output(report, !!options.json, report.artifacts.map((entry) => `${entry.format}: ${entry.path}`).join('\n'));
214
234
  if (!report.valid)
215
235
  process.exitCode = 1;
@@ -258,6 +278,61 @@ export function createProgram() {
258
278
  common(evidence.command('refresh'))
259
279
  .option('--changed', 'Preserve changed-file impact context while refreshing all checks')
260
280
  .action(executeGate);
281
+ common(evidence.command('unlock')
282
+ .description('Inspect and recover an abandoned project evidence-writer lock')
283
+ .requiredOption('--recover', 'Recover only a demonstrably dead same-host owner')
284
+ .addHelpText('after', `
285
+ Recovery never steals a live, remote, malformed, PID-reused, or indeterminate
286
+ lock. Inspect the reported owner and exact lock path before any targeted manual
287
+ removal. If a merge journal is also pending, recover this writer lock first,
288
+ then run evidence merge --recover.
289
+
290
+ Windows directory synchronization may be unavailable after atomic publication
291
+ or verified release. Automatic recovery remains inspection-only on Windows and
292
+ macOS.`))
293
+ .action(async (options) => {
294
+ if (!options.recover)
295
+ throw new Error('--recover is required.');
296
+ const report = await recoverEvidenceWriterLock(resolve(options.root));
297
+ output(report, !!options.json, report.action);
298
+ });
299
+ common(evidence.command('merge')
300
+ .description('Deterministically merge another valid append-only evidence history into the current root')
301
+ .option('--incoming <directory>', 'Project directory containing the incoming .musubix/evidence history')
302
+ .option('--dry-run', 'Validate and preview the merge without writing evidence')
303
+ .option('--recover', 'Recover an interrupted merge without requiring --incoming')
304
+ .addHelpText('after', `
305
+ Both roots must contain individually valid order, TDD, and change evidence. The
306
+ incoming root must be path-disjoint and is never modified. Merge uses canonical
307
+ duplicate comparison and stable base-first ordering; recordedAt warnings may
308
+ remain. The current worktree must already contain incoming change documents.
309
+ Merge only handles order.json, tdd.json, changes.json, and change-waivers.json.
310
+ Run gate/status afterward and explicitly re-approve stale waivers. If base
311
+ Quality precedes incoming batch evidence, merge before recording Quality.
312
+ Use --recover for interrupted transactions; unsafe recovery requires backing up
313
+ the evidence directory, restoring/verifying all four targets from a trusted
314
+ source, quarantining merge files, and rerunning structural validation.`))
315
+ .action(async (options) => {
316
+ const root = resolve(options.root);
317
+ if (options.recover) {
318
+ if (options.incoming || options.dryRun)
319
+ throw new Error('--recover cannot be combined with --incoming or --dry-run.');
320
+ const report = await recoverEvidenceMerge(root);
321
+ output(report, !!options.json, report.action);
322
+ return;
323
+ }
324
+ if (!options.incoming)
325
+ throw new Error('--incoming <directory> is required unless --recover is used.');
326
+ const report = await mergeEvidenceHistories(root, resolve(options.incoming), { ...(options.dryRun ? { dryRun: true } : {}) });
327
+ output(report, !!options.json, `${report.valid ? (options.dryRun ? 'DRY-RUN' : 'MERGED') : 'FAILED'}`
328
+ + ` preserved=${report.preserved} deduplicated=${report.deduplicated} appended=${report.appended}`
329
+ + `${report.diagnostics.length ? `\n${report.diagnostics.map((item) => `${item.code}${item.file ? ` ${item.file}` : ''}${item.identity ? ` ${item.identity}` : ''}: ${item.message}`).join('\n')}` : ''}`
330
+ + `${report.followUpDiagnostics.length ? `\n${report.followUpDiagnostics.map((item) => `${item.code}${item.path ? ` ${item.path}` : ''}: ${item.message}`).join('\n')}` : ''}`
331
+ + `${report.supersededScopes.length ? `\nWaiver supersession changed for ${report.supersededScopes.length} scope(s).` : ''}`
332
+ + `${report.revalidationRequired ? '\nEVIDENCE_MERGE_REVALIDATION_REQUIRED: rerun gate and status; re-approve stale waivers.' : ''}`);
333
+ if (!report.valid)
334
+ process.exitCode = 1;
335
+ });
261
336
  const mutation = program.command('mutation').description('Inspect and validate requirement-scoped mutation evidence');
262
337
  common(mutation.command('doctor')).action(async (options) => {
263
338
  const report = await mutationDoctor(resolve(options.root));
@@ -268,7 +343,9 @@ export function createProgram() {
268
343
  : 'No supported project ecosystem was detected.');
269
344
  });
270
345
  common(mutation.command('validate')).action(async (options) => {
271
- const report = await validateMutationEvidence(resolve(options.root));
346
+ const root = resolve(options.root);
347
+ await assertCoordinatedEvidenceRead(root);
348
+ const report = await validateMutationEvidence(root);
272
349
  if (!options.json && !report.present) {
273
350
  console.log('No mutation evidence at .musubix/evidence/mutation.json; the gate converts a configured mutationReport into that file. Compatible mode does not require it.');
274
351
  }
@@ -289,7 +366,9 @@ export function createProgram() {
289
366
  common(correspondence.command('validate')
290
367
  /* @id CODE-MODEL-CORRESPONDENCE-EVIDENCE-GUIDANCE-002 */
291
368
  .description('Validate model correspondence evidence (run `npx musubix3 evidence refresh` first to generate .musubix/evidence/model-correspondence.json)')).action(async (options) => {
292
- const report = await validateModelCorrespondenceEvidence(resolve(options.root));
369
+ const root = resolve(options.root);
370
+ await assertCoordinatedEvidenceRead(root);
371
+ const report = await validateModelCorrespondenceEvidence(root);
293
372
  result(report, !!options.json);
294
373
  });
295
374
  common(program.command('workflow-record <skill> <phase>').description('Record a self-reported workflow declaration'))
@@ -313,28 +392,30 @@ export function createProgram() {
313
392
  .option('--session-id <uuid>', 'Require the terminal result to identify this Copilot session')
314
393
  .action(async (logs, options) => {
315
394
  const root = resolve(options.root);
316
- const paths = logs.map((log) => resolve(log));
317
- for (const path of paths) {
318
- const info = await stat(path);
319
- if (!info.isFile())
320
- throw new Error('Workflow log must be a file.');
321
- }
322
- const configured = (await loadConfig(root)).workflow;
323
- const mode = options.strict || options.sessionId ? 'strict' : configured.mode;
324
- const expectedSessionId = options.sessionId ?? configured.expectedSessionId;
325
- const manifest = await verifyWorkflowLogFile(root, paths.length === 1 ? paths[0] : paths, {
326
- mode,
327
- ...(expectedSessionId ? { expectedSessionId } : {}),
328
- ...(configured.maxAgeSeconds === undefined ? {} : { maxAgeSeconds: configured.maxAgeSeconds }),
329
- ...(configured.maxFutureSkewSeconds === undefined
330
- ? {}
331
- : { maxFutureSkewSeconds: configured.maxFutureSkewSeconds }),
332
- ...(configured.maxEventSkewMs === undefined ? {} : { maxEventSkewMs: configured.maxEventSkewMs }),
333
- ...(configured.maxTranscriptBytes === undefined ? {} : { maxBytes: configured.maxTranscriptBytes }),
334
- ...(configured.maxTranscriptLineBytes === undefined ? {} : { maxLineBytes: configured.maxTranscriptLineBytes }),
395
+ await withEvidenceWriterLock(root, 'workflow-verify', async () => {
396
+ const paths = logs.map((log) => resolve(log));
397
+ for (const path of paths) {
398
+ const info = await stat(path);
399
+ if (!info.isFile())
400
+ throw new Error('Workflow log must be a file.');
401
+ }
402
+ const configured = (await loadConfig(root)).workflow;
403
+ const mode = options.strict || options.sessionId ? 'strict' : configured.mode;
404
+ const expectedSessionId = options.sessionId ?? configured.expectedSessionId;
405
+ const manifest = await verifyWorkflowLogFile(root, paths.length === 1 ? paths[0] : paths, {
406
+ mode,
407
+ ...(expectedSessionId ? { expectedSessionId } : {}),
408
+ ...(configured.maxAgeSeconds === undefined ? {} : { maxAgeSeconds: configured.maxAgeSeconds }),
409
+ ...(configured.maxFutureSkewSeconds === undefined
410
+ ? {}
411
+ : { maxFutureSkewSeconds: configured.maxFutureSkewSeconds }),
412
+ ...(configured.maxEventSkewMs === undefined ? {} : { maxEventSkewMs: configured.maxEventSkewMs }),
413
+ ...(configured.maxTranscriptBytes === undefined ? {} : { maxBytes: configured.maxTranscriptBytes }),
414
+ ...(configured.maxTranscriptLineBytes === undefined ? {} : { maxLineBytes: configured.maxTranscriptLineBytes }),
415
+ });
416
+ output(manifest, !!options.json, `Verified ${manifest.verification?.invocations.length ?? 0} Copilot Skill invocation event(s)`
417
+ + `${manifest.verification?.sessionId ? ` in session ${manifest.verification.sessionId}` : ''}.`);
335
418
  });
336
- output(manifest, !!options.json, `Verified ${manifest.verification?.invocations.length ?? 0} Copilot Skill invocation event(s)`
337
- + `${manifest.verification?.sessionId ? ` in session ${manifest.verification.sessionId}` : ''}.`);
338
419
  });
339
420
  common(program.command('workflow-sanitize <log> <output-file>').description('Write a privacy-minimized Skill lifecycle transcript'))
340
421
  .option('--session-id <uuid>', 'Replace the terminal session ID with a review-safe UUID')
@@ -416,6 +497,7 @@ export function createProgram() {
416
497
  if (!['github', 'azure-pipelines', 'generic'].includes(options.provider))
417
498
  throw new Error('Unknown attestation provider.');
418
499
  const root = resolve(options.root);
500
+ await assertCoordinatedEvidenceRead(root);
419
501
  const configured = (await loadConfig(root)).attestation.githubOidc;
420
502
  if (configured?.mode === 'strict' && options.provider !== 'github') {
421
503
  throw new Error('Strict GitHub OIDC attestation requires --provider github.');
@@ -448,6 +530,7 @@ export function createProgram() {
448
530
  });
449
531
  common(attestation.command('verify')).action(async (options) => {
450
532
  const root = resolve(options.root);
533
+ await assertCoordinatedEvidenceRead(root);
451
534
  const report = await verifyEvidenceAttestation(root, (await loadConfig(root)).attestation);
452
535
  output(report, !!options.json, `${report.status}: ${report.valid ? 'valid' : 'invalid'}`);
453
536
  if (!report.valid)
@@ -481,6 +564,16 @@ export function createProgram() {
481
564
  output(evidence, !!options.json, options.dryRun ? `Would record ${changeId}:${phase}.` : `Recorded ${changeId}:${phase}.`);
482
565
  });
483
566
  const change = program.command('change').description('Change chronology and waiver evidence');
567
+ /** @id CODE-CHANGE-QUALITY-REFRESH-005
568
+ * @implements REQ-CHANGE-QUALITY-REFRESH-003
569
+ * @design DES-CHANGE-QUALITY-REFRESH-002
570
+ */
571
+ common(change.command('quality-recover')
572
+ .description('Recover an interrupted two-file Quality refresh transaction'))
573
+ .action(async (options) => {
574
+ const report = await recoverQualityRefresh(resolve(options.root));
575
+ output(report, !!options.json, report.recovered ? `Quality refresh ${report.action}.` : 'No Quality refresh to recover.');
576
+ });
484
577
  const waiver = change.command('waiver').description('Record an audited, bounded downgrade of a recording-order-debt diagnostic');
485
578
  common(waiver.command('record <change-id> <code>')
486
579
  .description('Downgrade one currently-present waivable diagnostic from error to warning, as an appended, hash-chained record'))
@@ -496,17 +589,41 @@ export function createProgram() {
496
589
  output(result, !!options.json, `WAIVER: PASS (${changeId}:${code}${options.requirement ? `:${options.requirement}` : ''}${options.detail ? `:${options.detail}` : ''})`);
497
590
  });
498
591
  const approval = program.command('approval').description('Prepare, record and validate explicit artifact-bound human approvals');
592
+ /** @id CODE-APPROVAL-PREPARE-DIFF-ONLY-003
593
+ * @implements REQ-APPROVAL-PREPARE-DIFF-ONLY-001 REQ-APPROVAL-PREPARE-DIFF-ONLY-002 REQ-APPROVAL-PREPARE-DIFF-ONLY-004
594
+ * @implements REQ-APPROVAL-PREPARE-DIFF-ONLY-005 REQ-APPROVAL-PREPARE-DIFF-ONLY-006 REQ-APPROVAL-PREPARE-DIFF-ONLY-007
595
+ * @design DES-APPROVAL-PREPARE-DIFF-ONLY-002 DES-APPROVAL-PREPARE-DIFF-ONLY-003
596
+ */
499
597
  common(approval.command('prepare <stage>').description('Show the exact artifact manifest a human must review'))
500
598
  .option('--domain <name>', 'Approval domain (required when approval.domains is configured, except for release)')
599
+ .option('--diff-only', 'Additionally report only the file paths whose content changed since the last recorded approval for this stage')
501
600
  .action(async (stage, options) => {
502
601
  if (!approvalStages.includes(stage))
503
602
  throw new Error(`stage must be one of: ${approvalStages.join(', ')}`);
504
603
  const root = resolve(options.root);
604
+ await assertCoordinatedEvidenceRead(root);
505
605
  const config = await loadConfig(root);
506
606
  requireDomainOption(config.approval, stage, options.domain);
507
607
  const domain = options.domain ? await resolveNamedDomain(root, config.approval, options.domain) : undefined;
508
608
  const manifest = await approvalManifest(root, stage, domain);
509
- output(manifest, !!options.json, `${stage} artifact manifest: ${manifest.artifactSha256}\n${Object.keys(manifest.artifacts).join('\n')}`);
609
+ if (!options.diffOnly) {
610
+ output(manifest, !!options.json, `${stage} artifact manifest: ${manifest.artifactSha256}\n${Object.keys(manifest.artifacts).join('\n')}`);
611
+ return;
612
+ }
613
+ // REQ-APPROVAL-PREPARE-DIFF-ONLY-007: no try/catch here — a corrupted prior
614
+ // approval file must abort the command via loadApproval's thrown error,
615
+ // never be silently treated as "no prior approval".
616
+ const previous = await loadApproval(root, stage, domain?.name);
617
+ const { changedFiles: changed, diffOnlyBaseline } = diffOnlyChangedFiles(manifest, previous);
618
+ const withDiff = { ...manifest, changedFiles: changed, diffOnlyBaseline };
619
+ // REQ-APPROVAL-PREPARE-DIFF-ONLY-006: the non-JSON summary shows only the
620
+ // changed paths (plus the full hash and an explicit baseline note), never
621
+ // the complete artifact listing; --json output above is unaffected.
622
+ const baselineNote = diffOnlyBaseline === 'none'
623
+ ? 'no prior approval found; showing all current artifacts'
624
+ : 'compared against the previously recorded approval';
625
+ const diffSummary = `${stage} artifact manifest: ${manifest.artifactSha256}\n(diff-only: ${baselineNote})\n${changed.join('\n')}`;
626
+ output(withDiff, !!options.json, diffSummary);
510
627
  });
511
628
  common(approval.command('record <stage>'))
512
629
  .requiredOption('--approver <name>', 'Human approver name')
@@ -520,14 +637,17 @@ export function createProgram() {
520
637
  if (options.confirm !== true)
521
638
  throw new Error('--confirm is required to record human approval.');
522
639
  const root = resolve(options.root);
523
- const config = await loadConfig(root);
524
- const evidence = await recordApproval(root, stage, options.approver, options.artifactSha256, config.approval, options.domain);
525
- output(evidence, !!options.json, `Recorded explicit ${stage} approval by ${evidence.approver} for ${evidence.artifactSha256}.`);
640
+ await withEvidenceWriterLock(root, 'approval record', async () => {
641
+ const config = await loadConfig(root);
642
+ const evidence = await recordApproval(root, stage, options.approver, options.artifactSha256, config.approval, options.domain);
643
+ output(evidence, !!options.json, `Recorded explicit ${stage} approval by ${evidence.approver} for ${evidence.artifactSha256}.`);
644
+ });
526
645
  });
527
646
  common(approval.command('validate'))
528
647
  .option('--domain <name>', 'Limit the report to one approval domain')
529
648
  .action(async (options) => {
530
649
  const root = resolve(options.root);
650
+ await assertCoordinatedEvidenceRead(root);
531
651
  const config = await loadConfig(root);
532
652
  requireValidateDomainOption(config.approval, options.domain);
533
653
  const report = options.domain
@@ -544,7 +664,9 @@ export function createProgram() {
544
664
  + "name suffix for go-test/cargo, an additional @Tag for junit, DisplayName for dotnet). "
545
665
  + 'See README.md "Adapter test-ID declaration reference" for the full table and worked examples.');
546
666
  common(tdd.command('validate')).action(async (options) => {
547
- const report = await validateTddEvidence(resolve(options.root));
667
+ const root = resolve(options.root);
668
+ await assertCoordinatedEvidenceRead(root);
669
+ const report = await validateTddEvidence(root);
548
670
  result(report, !!options.json);
549
671
  });
550
672
  for (const phase of ['red', 'green', 'refactor']) {
@@ -559,13 +681,14 @@ export function createProgram() {
559
681
  // onto `green`/`refactor` via the shared loop.
560
682
  if (phase === 'red') {
561
683
  phaseCommand.description('Record a failing (Red) test as TDD evidence for a requirement. Persisting the '
562
- + "project's first cycle here makes gate's tdd check required project-wide for "
563
- + 'every mandatory requirement (each uncovered one surfaced as '
564
- + 'TDD_REQUIREMENT_UNCOVERED); "approval record release" always runs the full '
565
- + '(non-\'--changed\') gate, so it is blocked by any resulting '
566
- + 'TDD_REQUIREMENT_UNCOVERED diagnostics. "tdd migrate" cannot bulk-onboard '
567
- + 'previously-uncovered requirements: it only re-fingerprints a requirement that '
568
- + 'already has a valid Green cycle.');
684
+ + "project's first cycle here activates gate's tdd coverage evaluation project-wide "
685
+ + 'for every mandatory requirement (each uncovered one surfaced as '
686
+ + 'TDD_REQUIREMENT_UNCOVERED); if tdd was not already required for another '
687
+ + 'configured reason, that same first cycle is what makes the check required. '
688
+ + '"approval record release" always runs the full (non-\'--changed\') gate, so '
689
+ + 'it is blocked by any resulting TDD_REQUIREMENT_UNCOVERED diagnostics. '
690
+ + '"tdd migrate" cannot bulk-onboard previously-uncovered requirements: it only '
691
+ + 'reuses already-covered valid Green-backed evidence.');
569
692
  }
570
693
  phaseCommand
571
694
  .requiredOption('--requirement <id>', 'Requirement ID verified by the test')
@@ -580,17 +703,23 @@ export function createProgram() {
580
703
  process.exitCode = 1;
581
704
  });
582
705
  }
583
- common(tdd.command('migrate <test-id>'))
584
- .requiredOption('--approver <name>', 'Human approver recording this fingerprint migration')
706
+ common(tdd.command('migrate [ids...]'))
707
+ .description('Relink existing covered evidence: `tdd migrate <test-id>` re-fingerprints one covered test; `tdd migrate <old-id> <new-id>` relinks a pure identifier rename.')
708
+ .requiredOption('--approver <name>', 'Human approver recording this migrate operation')
585
709
  .option('--confirm', 'Confirm the migration is reviewed and intended', false)
586
- .action(async (testId, options) => {
710
+ .action(async (ids, options) => {
587
711
  if (!options.confirm)
588
- throw new Error('Fingerprint migration requires --confirm.');
712
+ throw new Error('TDD migration requires --confirm.');
713
+ if (ids.length < 1 || ids.length > 2)
714
+ throw new InvalidArgumentError('tdd migrate accepts either <test-id> or <old-id> <new-id>.');
589
715
  const root = resolve(options.root);
590
- const migration = await migrateTddFingerprint(root, testId, options.approver);
716
+ const migration = ids.length === 1
717
+ ? await migrateTddFingerprint(root, ids[0], options.approver)
718
+ : await migrateTddIdentifier(root, ids[0], ids[1], options.approver);
719
+ const target = ids.join(' -> ');
591
720
  output(migration, !!options.json, migration.migrated
592
- ? `MIGRATE: PASS (${testId}) ${migration.fromFingerprint} -> ${migration.toFingerprint}`
593
- : `MIGRATE: FAIL (${testId}) ${migration.reason}`);
721
+ ? `MIGRATE: PASS (${target}) ${migration.fromFingerprint} -> ${migration.toFingerprint}`
722
+ : `MIGRATE: FAIL (${target}) ${migration.reason}`);
594
723
  if (!migration.migrated)
595
724
  process.exitCode = 1;
596
725
  });
@@ -609,9 +738,26 @@ export function createProgram() {
609
738
  if (!voidResult.voided)
610
739
  process.exitCode = 1;
611
740
  });
741
+ common(tdd.command('archive <test-id>'))
742
+ .requiredOption('--approver <name>', 'Human approver recording this archive')
743
+ .requiredOption('--reason <text>', 'Reason this TDD cycle is being archived')
744
+ .option('--confirm', 'Confirm the archive is reviewed and intended', false)
745
+ .action(async (testId, options) => {
746
+ if (!options.confirm)
747
+ throw new Error('Archiving a TDD cycle requires --confirm.');
748
+ const root = resolve(options.root);
749
+ const archiveResult = await archiveTddCycle(root, testId, options.approver, options.reason);
750
+ output(archiveResult, !!options.json, archiveResult.archived
751
+ ? `ARCHIVE: PASS (${testId}) cycle=${archiveResult.cycleId}`
752
+ : `ARCHIVE: FAIL (${testId}) ${archiveResult.reason}`);
753
+ if (!archiveResult.archived)
754
+ process.exitCode = 1;
755
+ });
612
756
  common(program.command('status').description('One-shot artifact and gate readiness summary'))
613
757
  .action(async (options) => {
614
- const status = await projectStatus(resolve(options.root));
758
+ const root = resolve(options.root);
759
+ await assertCoordinatedEvidenceRead(root);
760
+ const status = await projectStatus(root);
615
761
  const approvalSummary = status.approvals
616
762
  ? status.approvals.stages.map((stage) => `${stage.stage}=${stage.status}`).join(', ')
617
763
  : 'unconfigured';
@@ -627,13 +773,40 @@ async function main() {
627
773
  if (cause instanceof CommanderError && cause.exitCode === 0)
628
774
  return;
629
775
  const message = cause instanceof Error ? cause.message : String(cause);
630
- if (process.argv.includes('--json'))
631
- console.log(JSON.stringify({ error: { code: 'CLI_ERROR', message } }));
776
+ if (process.argv.includes('--json')) {
777
+ if (cause instanceof EvidenceWriterLockError) {
778
+ const underlying = cause.cause instanceof Error
779
+ ? {
780
+ name: cause.cause.name,
781
+ message: cause.cause.message,
782
+ ...('code' in cause.cause ? { code: String(cause.cause.code) } : {}),
783
+ }
784
+ : cause.cause === undefined ? undefined : { message: String(cause.cause) };
785
+ console.log(JSON.stringify({
786
+ error: {
787
+ code: cause.code,
788
+ message,
789
+ lockPath: cause.lockPath,
790
+ ...(cause.owner === undefined ? {} : { owner: cause.owner }),
791
+ ...(underlying === undefined ? {} : { cause: underlying }),
792
+ ...(cause.guidance === undefined ? {} : { guidance: cause.guidance }),
793
+ },
794
+ }));
795
+ }
796
+ else if (cause instanceof EvidenceProtectedOutputError) {
797
+ console.log(JSON.stringify({ error: { code: cause.code, message, path: cause.path } }));
798
+ }
799
+ else {
800
+ console.log(JSON.stringify({ error: { code: 'CLI_ERROR', message } }));
801
+ }
802
+ }
632
803
  else
633
804
  console.error(`musubix3: ${message}`);
634
805
  process.exitCode = 2;
635
806
  }
636
807
  }
637
- await main();
808
+ if (process.argv[1] !== undefined && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url) {
809
+ await main();
810
+ }
638
811
  import { readFile, stat } from 'node:fs/promises';
639
812
  //# sourceMappingURL=main.js.map