@ktpartners/dgs-platform 3.0.4 → 3.3.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.
- package/CHANGELOG.md +115 -0
- package/README.md +8 -1
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +32 -0
- package/agents/dgs-planner.md +41 -8
- package/bin/install.js +44 -0
- package/commands/dgs/audit-milestone.md +2 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/research-idea.md +1 -0
- package/commands/dgs/switch-project.md +13 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +311 -16
- package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +41 -0
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/core.cjs +7 -3
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/init.cjs +56 -27
- package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
- package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
- package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +18 -1
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +38 -3
- package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
- package/deliver-great-systems/bin/lib/quick.cjs +178 -23
- package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
- package/deliver-great-systems/bin/lib/repos.cjs +12 -12
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/state.cjs +7 -3
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/verify.cjs +118 -6
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +27 -1
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +11 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +26 -7
- package/deliver-great-systems/workflows/complete-quick.md +40 -2
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +89 -2
- package/deliver-great-systems/workflows/execute-plan.md +10 -1
- package/deliver-great-systems/workflows/help.md +51 -18
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +46 -152
- package/deliver-great-systems/workflows/new-milestone.md +115 -14
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/quick-complete.md +40 -2
- package/deliver-great-systems/workflows/quick.md +183 -10
- package/deliver-great-systems/workflows/research-idea.md +80 -142
- package/deliver-great-systems/workflows/run-job.md +21 -35
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/write-spec.md +9 -11
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -283,18 +283,44 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
283
283
|
* Config loading still uses cwd. Used by fast-path in milestone-context to
|
|
284
284
|
* commit in a worktree while loading config from the planning root.
|
|
285
285
|
*/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
// Collect the list of still-dirty paths in `gitCwd` immediately after a
|
|
287
|
+
// commit (or nothing_to_commit). Purely informational: populates
|
|
288
|
+
// `result.dirty_after` so callers can detect verify-step side effects
|
|
289
|
+
// (formatter reflows, type narrowings) that leaked outside the staged
|
|
290
|
+
// file set. Never throws — returns [] on any error.
|
|
291
|
+
function collectDirtyAfter(gitCwd) {
|
|
292
|
+
const porcelain = execGit(gitCwd, ['status', '--porcelain']);
|
|
293
|
+
if (porcelain.exitCode !== 0) return [];
|
|
294
|
+
return (porcelain.stdout || '')
|
|
295
|
+
.split('\n')
|
|
296
|
+
.map(l => l.trim())
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.map(l => l.replace(/^..\s+/, '')); // strip two-char XY status prefix + space
|
|
299
|
+
}
|
|
290
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Internal commit primitive — RETURNS the JSON result object instead of
|
|
303
|
+
* calling output()/process.exit. Extracted from cmdCommit for reuse by
|
|
304
|
+
* verifyPlanCommit (REL-01, Phase 156). External callers should keep
|
|
305
|
+
* using cmdCommit; this helper is library-internal only.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} cwd - Planning root (config loaded from here)
|
|
308
|
+
* @param {string} message - Commit message (required unless amend)
|
|
309
|
+
* @param {string[]|undefined} files - Files to stage; if empty/undefined,
|
|
310
|
+
* the historical behaviour is to fall back to ['.'] (sweeping the
|
|
311
|
+
* working tree). REL-01's verifyPlanCommit guards against this fallback
|
|
312
|
+
* for the orchestrator commit case BEFORE it ever reaches commitInternal.
|
|
313
|
+
* @param {boolean} amend
|
|
314
|
+
* @param {boolean} push
|
|
315
|
+
* @param {string} [repoCwd] - Where git operations actually run.
|
|
316
|
+
* @returns {object} JSON result matching cmdCommit's existing contract.
|
|
317
|
+
*/
|
|
318
|
+
function commitInternal(cwd, message, files, amend, push, repoCwd) {
|
|
291
319
|
const config = loadConfig(cwd);
|
|
292
320
|
|
|
293
321
|
// Check commit_docs config
|
|
294
322
|
if (!config.commit_docs) {
|
|
295
|
-
|
|
296
|
-
output(result, raw, 'skipped');
|
|
297
|
-
return;
|
|
323
|
+
return { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
|
|
298
324
|
}
|
|
299
325
|
|
|
300
326
|
// Resolve git-operation cwd: use repoCwd when provided, otherwise cwd.
|
|
@@ -313,19 +339,15 @@ function cmdCommit(cwd, message, files, raw, amend, push, repoCwd) {
|
|
|
313
339
|
const commitResult = execGit(gitCwd, commitArgs);
|
|
314
340
|
if (commitResult.exitCode !== 0) {
|
|
315
341
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
316
|
-
|
|
317
|
-
output(result, raw, 'nothing');
|
|
318
|
-
return;
|
|
342
|
+
return { committed: false, hash: null, reason: 'nothing_to_commit', dirty_after: collectDirtyAfter(gitCwd) };
|
|
319
343
|
}
|
|
320
|
-
|
|
321
|
-
output(result, raw, 'nothing');
|
|
322
|
-
return;
|
|
344
|
+
return { committed: false, hash: null, reason: 'nothing_to_commit', error: commitResult.stderr, dirty_after: collectDirtyAfter(gitCwd) };
|
|
323
345
|
}
|
|
324
346
|
|
|
325
347
|
// Get short hash
|
|
326
348
|
const hashResult = execGit(gitCwd, ['rev-parse', '--short', 'HEAD']);
|
|
327
349
|
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
|
328
|
-
const result = { committed: true, hash, reason: 'committed' };
|
|
350
|
+
const result = { committed: true, hash, reason: 'committed', dirty_after: collectDirtyAfter(gitCwd) };
|
|
329
351
|
|
|
330
352
|
// Handle push if requested
|
|
331
353
|
if (push) {
|
|
@@ -347,7 +369,234 @@ function cmdCommit(cwd, message, files, raw, amend, push, repoCwd) {
|
|
|
347
369
|
// 'off' or any other value: no push fields added
|
|
348
370
|
}
|
|
349
371
|
|
|
350
|
-
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function cmdCommit(cwd, message, files, raw, amend, push, repoCwd) {
|
|
376
|
+
if (!message && !amend) {
|
|
377
|
+
error('commit message required');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const result = commitInternal(cwd, message, files, amend, push, repoCwd);
|
|
381
|
+
|
|
382
|
+
// Branch labels per the original cmdCommit output() calls
|
|
383
|
+
if (result.reason === 'skipped_commit_docs_false') {
|
|
384
|
+
output(result, raw, 'skipped');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (result.reason === 'nothing_to_commit') {
|
|
388
|
+
output(result, raw, 'nothing');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
output(result, raw, result.hash || 'committed');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* REL-01 (Phase 156, plan 02): orchestrator-side commit + verification
|
|
396
|
+
* helper consumed by /dgs:plan-phase to commit planner-reported
|
|
397
|
+
* created_files. Guarantees:
|
|
398
|
+
*
|
|
399
|
+
* - Empty createdFiles → returns plan-commit-incomplete WITHOUT calling
|
|
400
|
+
* commitInternal. Defends against cmdCommit's `['.']` fallback that
|
|
401
|
+
* would otherwise sweep the working tree (Hypothesis C from
|
|
402
|
+
* 156-Q1-FINDINGS.md).
|
|
403
|
+
* - commit_docs:false → silent success ({ ok: true, hash: null,
|
|
404
|
+
* reason: 'skipped_commit_docs_false' }). NOT a failure.
|
|
405
|
+
* - cmdCommit failure → plan-commit-incomplete with reason: 'commit_failed'.
|
|
406
|
+
* - Verification: every entry in (createdFiles + extraFiles) MUST appear
|
|
407
|
+
* in `git show --name-only HEAD`; mismatches → plan-commit-incomplete
|
|
408
|
+
* with reason: 'verification_failed' and a `missing` array.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} cwd - Planning root (config loaded from here)
|
|
411
|
+
* @param {object} options - {
|
|
412
|
+
* message: string (REQUIRED),
|
|
413
|
+
* createdFiles: string[] (REQUIRED),
|
|
414
|
+
* extraFiles?: string[],
|
|
415
|
+
* repoCwd?: string,
|
|
416
|
+
* push?: boolean,
|
|
417
|
+
* }
|
|
418
|
+
* @param {boolean} raw - Emit raw JSON via output() if true.
|
|
419
|
+
* @returns {object} Returns the result object directly (so the test
|
|
420
|
+
* harness can assert on it). Also calls output() when invoked from CLI.
|
|
421
|
+
*/
|
|
422
|
+
function verifyPlanCommit(cwd, options, raw) {
|
|
423
|
+
const opts = options || {};
|
|
424
|
+
const message = opts.message;
|
|
425
|
+
const createdFiles = opts.createdFiles;
|
|
426
|
+
const extraFiles = opts.extraFiles || [];
|
|
427
|
+
const repoCwd = opts.repoCwd;
|
|
428
|
+
const push = !!opts.push;
|
|
429
|
+
|
|
430
|
+
// verifyPlanCommit is a pure helper — it RETURNS the result object
|
|
431
|
+
// (so library callers and tests can assert on it). The `raw` argument
|
|
432
|
+
// is accepted for CLI dispatch parity but is intentionally unused
|
|
433
|
+
// here; the dgs-tools CLI dispatcher (dgs-tools.cjs) is responsible
|
|
434
|
+
// for calling output()/process.exit on the returned object.
|
|
435
|
+
void raw;
|
|
436
|
+
|
|
437
|
+
if (!message) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
exitLabel: 'plan-commit-incomplete',
|
|
441
|
+
reason: 'missing_message',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Empty-list guard — defends Hypothesis C (cmdCommit `['.']` fallback)
|
|
446
|
+
if (!Array.isArray(createdFiles) || createdFiles.length === 0) {
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
exitLabel: 'plan-commit-incomplete',
|
|
450
|
+
reason: 'empty_created_files',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const filesToCommit = [...createdFiles, ...extraFiles];
|
|
455
|
+
|
|
456
|
+
const commitResult = commitInternal(cwd, message, filesToCommit, false, push, repoCwd);
|
|
457
|
+
|
|
458
|
+
// commit_docs config gate — silent success
|
|
459
|
+
if (commitResult.reason === 'skipped_commit_docs_false') {
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
hash: null,
|
|
463
|
+
reason: 'skipped_commit_docs_false',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Any non-success commit reason is a plan-commit-incomplete failure
|
|
468
|
+
if (commitResult.committed !== true) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
exitLabel: 'plan-commit-incomplete',
|
|
472
|
+
reason: 'commit_failed',
|
|
473
|
+
commit_reason: commitResult.reason,
|
|
474
|
+
error: commitResult.error,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Verification: every reported path MUST appear in HEAD
|
|
479
|
+
const gitCwd = repoCwd || cwd;
|
|
480
|
+
const showResult = execGit(gitCwd, ['show', '--name-only', '--pretty=', 'HEAD']);
|
|
481
|
+
const committedFiles = (showResult.stdout || '')
|
|
482
|
+
.split('\n')
|
|
483
|
+
.map(l => l.trim())
|
|
484
|
+
.filter(Boolean);
|
|
485
|
+
const missing = filesToCommit.filter(f => !committedFiles.includes(f));
|
|
486
|
+
|
|
487
|
+
if (missing.length > 0) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
exitLabel: 'plan-commit-incomplete',
|
|
491
|
+
reason: 'verification_failed',
|
|
492
|
+
hash: commitResult.hash,
|
|
493
|
+
missing,
|
|
494
|
+
committedFiles,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
ok: true,
|
|
500
|
+
hash: commitResult.hash,
|
|
501
|
+
reason: 'committed',
|
|
502
|
+
files_verified: filesToCommit,
|
|
503
|
+
missing: [],
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* REL-02 (Phase 156, plan 03): compute the executor's final-commit
|
|
509
|
+
* phase-dir sweep. Always sweeps the current phase directory and takes
|
|
510
|
+
* the UNION with the executor-reported modified_files list, then
|
|
511
|
+
* scope-filters out anything that does not start with the
|
|
512
|
+
* ${phasesDir}/${phaseDir}/ prefix.
|
|
513
|
+
*
|
|
514
|
+
* Hard scope guarantee: dirty files in sibling phases, ideas/, specs/,
|
|
515
|
+
* or the project root are NEVER returned in `swept`. They are returned
|
|
516
|
+
* in `dropped` for diagnostic visibility.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} cwd - Planning root (where git status runs)
|
|
519
|
+
* @param {object} options - {
|
|
520
|
+
* phasesDir: string, // e.g. 'projects/gsd/phases'
|
|
521
|
+
* phaseDir: string, // e.g. '156-idea-26-closure-...'
|
|
522
|
+
* modifiedFiles?: string[] // executor-reported paths (planning-root-relative)
|
|
523
|
+
* }
|
|
524
|
+
* @param {boolean} raw
|
|
525
|
+
* @returns {{
|
|
526
|
+
* swept: string[], // commit list — UNION, scope-filtered, sorted
|
|
527
|
+
* dropped: string[], // out-of-scope paths the helper rejected
|
|
528
|
+
* gitDirtyPaths: string[], // git-discovered phase-dir paths (pre-union)
|
|
529
|
+
* reportedPaths: string[], // executor-reported list (pre-union)
|
|
530
|
+
* scopePrefix: string // ${phasesDir}/${phaseDir}/
|
|
531
|
+
* }}
|
|
532
|
+
*/
|
|
533
|
+
function computePhaseSweep(cwd, options, raw) {
|
|
534
|
+
const opts = options || {};
|
|
535
|
+
const phasesDir = opts.phasesDir;
|
|
536
|
+
const phaseDir = opts.phaseDir;
|
|
537
|
+
const reportedPathsRaw = opts.modifiedFiles || [];
|
|
538
|
+
|
|
539
|
+
void raw;
|
|
540
|
+
|
|
541
|
+
if (!phasesDir || !phaseDir) {
|
|
542
|
+
return {
|
|
543
|
+
swept: [],
|
|
544
|
+
dropped: [],
|
|
545
|
+
gitDirtyPaths: [],
|
|
546
|
+
reportedPaths: [],
|
|
547
|
+
scopePrefix: null,
|
|
548
|
+
error: 'phasesDir and phaseDir required for compute-phase-sweep',
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const scopePrefix = `${phasesDir}/${phaseDir}/`;
|
|
553
|
+
|
|
554
|
+
// Run path-scoped porcelain. Use --untracked-files=all so untracked
|
|
555
|
+
// files inside untracked directories are listed individually rather
|
|
556
|
+
// than collapsed into a single directory entry like `phases/{dir}/`.
|
|
557
|
+
const gitArgs = ['status', '--porcelain', '--untracked-files=all', '--', `${phasesDir}/${phaseDir}`];
|
|
558
|
+
const gitResult = execGit(cwd, gitArgs);
|
|
559
|
+
const gitDirtyPaths = gitResult.exitCode === 0
|
|
560
|
+
? (gitResult.stdout || '')
|
|
561
|
+
.split('\n')
|
|
562
|
+
.map(l => l.trim())
|
|
563
|
+
.filter(Boolean)
|
|
564
|
+
// Strip 2-char XY status prefix + space (matches collectDirtyAfter)
|
|
565
|
+
.map(l => l.replace(/^..\s+/, ''))
|
|
566
|
+
: [];
|
|
567
|
+
|
|
568
|
+
// Strip optional `repoName:` prefix from reported paths — multi-repo
|
|
569
|
+
// entries with explicit repoName are out of scope for the planning-root
|
|
570
|
+
// sweep (they route through their own resolveRepoRelativePath flow).
|
|
571
|
+
const reportedPaths = reportedPathsRaw
|
|
572
|
+
.map(p => {
|
|
573
|
+
// If the entry contains a `:` and the leading segment looks like a
|
|
574
|
+
// repo name (no `/`), treat it as repoName:path and drop it for
|
|
575
|
+
// planning-root sweep purposes.
|
|
576
|
+
const colonIdx = p.indexOf(':');
|
|
577
|
+
if (colonIdx > 0 && p.indexOf('/') > colonIdx) return null;
|
|
578
|
+
return p;
|
|
579
|
+
})
|
|
580
|
+
.filter(p => p !== null);
|
|
581
|
+
|
|
582
|
+
// Compute the UNION (dedupe via Set)
|
|
583
|
+
const candidates = Array.from(new Set([...gitDirtyPaths, ...reportedPaths]));
|
|
584
|
+
|
|
585
|
+
// Belt-and-braces: scope-filter via prefix check
|
|
586
|
+
const swept = candidates
|
|
587
|
+
.filter(p => p.startsWith(scopePrefix))
|
|
588
|
+
.sort();
|
|
589
|
+
const dropped = candidates
|
|
590
|
+
.filter(p => !p.startsWith(scopePrefix))
|
|
591
|
+
.sort();
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
swept,
|
|
595
|
+
dropped,
|
|
596
|
+
gitDirtyPaths,
|
|
597
|
+
reportedPaths,
|
|
598
|
+
scopePrefix,
|
|
599
|
+
};
|
|
351
600
|
}
|
|
352
601
|
|
|
353
602
|
/**
|
|
@@ -530,7 +779,9 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
530
779
|
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
|
|
531
780
|
patterns: fm['patterns-established'] || [],
|
|
532
781
|
decisions: parseDecisions(fm['key-decisions']),
|
|
533
|
-
|
|
782
|
+
// REL-07/10 transitional dual-read: canonical underscore key takes precedence;
|
|
783
|
+
// legacy hyphen key fallback preserves audit-readability for archived v23.1 SUMMARYs.
|
|
784
|
+
requirements_completed: fm['requirements_completed'] || fm['requirements-completed'] || [],
|
|
534
785
|
};
|
|
535
786
|
|
|
536
787
|
// If fields specified, filter to only those fields
|
|
@@ -849,6 +1100,46 @@ function cmdContextHelp(raw) {
|
|
|
849
1100
|
output(result, raw, result.subcommands.map(s => s.usage).join('\n'));
|
|
850
1101
|
}
|
|
851
1102
|
|
|
1103
|
+
// REL-08 (Phase 157): pre-commit precondition gate.
|
|
1104
|
+
// Reads PLAN.md `requirements:` and SUMMARY.md `requirements_completed:` (canonical) /
|
|
1105
|
+
// `requirements-completed:` (legacy fallback). If PLAN is non-empty AND SUMMARY is empty,
|
|
1106
|
+
// writes `summary-frontmatter-mismatch:` label to stderr and exits non-zero.
|
|
1107
|
+
// NEVER writes to the working tree — read-only check.
|
|
1108
|
+
function cmdFinalCommitPrecondition(cwd, options) {
|
|
1109
|
+
const planPath = options && options.plan;
|
|
1110
|
+
const summaryPath = options && options.summary;
|
|
1111
|
+
if (!planPath || !summaryPath) {
|
|
1112
|
+
process.stderr.write('summary-frontmatter-mismatch: --plan and --summary required\n');
|
|
1113
|
+
process.exit(2);
|
|
1114
|
+
}
|
|
1115
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
1116
|
+
const planAbs = path.isAbsolute(planPath) ? planPath : path.join(cwd, planPath);
|
|
1117
|
+
const summaryAbs = path.isAbsolute(summaryPath) ? summaryPath : path.join(cwd, summaryPath);
|
|
1118
|
+
|
|
1119
|
+
if (!fs.existsSync(planAbs)) {
|
|
1120
|
+
process.stderr.write(`summary-frontmatter-mismatch: PLAN not found at ${planPath}\n`);
|
|
1121
|
+
process.exit(2);
|
|
1122
|
+
}
|
|
1123
|
+
if (!fs.existsSync(summaryAbs)) {
|
|
1124
|
+
process.stderr.write(`summary-frontmatter-mismatch: SUMMARY not found at ${summaryPath}\n`);
|
|
1125
|
+
process.exit(2);
|
|
1126
|
+
}
|
|
1127
|
+
const planFm = extractFrontmatter(fs.readFileSync(planAbs, 'utf-8'));
|
|
1128
|
+
const summaryFm = extractFrontmatter(fs.readFileSync(summaryAbs, 'utf-8'));
|
|
1129
|
+
const planReq = Array.isArray(planFm.requirements) ? planFm.requirements : [];
|
|
1130
|
+
// Dual-read: canonical underscore key first, legacy hyphen fallback (REL-07/10 dual-read).
|
|
1131
|
+
const summaryReq = summaryFm['requirements_completed'] || summaryFm['requirements-completed'] || [];
|
|
1132
|
+
|
|
1133
|
+
if (planReq.length > 0 && summaryReq.length === 0) {
|
|
1134
|
+
process.stderr.write(
|
|
1135
|
+
`summary-frontmatter-mismatch: PLAN.md declared requirements [${planReq.join(', ')}]; ` +
|
|
1136
|
+
`SUMMARY.md requirements_completed is empty. Re-run executor or manually populate before committing.\n`
|
|
1137
|
+
);
|
|
1138
|
+
process.exit(2);
|
|
1139
|
+
}
|
|
1140
|
+
process.exit(0);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
852
1143
|
module.exports = {
|
|
853
1144
|
TODO_STATUSES,
|
|
854
1145
|
setTodoStatus,
|
|
@@ -859,6 +1150,9 @@ module.exports = {
|
|
|
859
1150
|
cmdHistoryDigest,
|
|
860
1151
|
cmdResolveModel,
|
|
861
1152
|
cmdCommit,
|
|
1153
|
+
commitInternal,
|
|
1154
|
+
verifyPlanCommit,
|
|
1155
|
+
computePhaseSweep,
|
|
862
1156
|
cmdPlanFinalize,
|
|
863
1157
|
cmdSummaryExtract,
|
|
864
1158
|
cmdWebsearch,
|
|
@@ -866,4 +1160,5 @@ module.exports = {
|
|
|
866
1160
|
cmdTodoComplete,
|
|
867
1161
|
cmdScaffold,
|
|
868
1162
|
cmdContextHelp,
|
|
1163
|
+
cmdFinalCommitPrecondition,
|
|
869
1164
|
};
|
|
@@ -541,6 +541,121 @@ describe('cmdCommit with --repo-cwd flag', () => {
|
|
|
541
541
|
});
|
|
542
542
|
});
|
|
543
543
|
|
|
544
|
+
// ─── cmdCommit populates dirty_after ────────────────────────────────────────
|
|
545
|
+
// Uses captureStdout (not captureCommitOutput) because cmdCommit's output() goes
|
|
546
|
+
// through process.stdout.write + process.exit, not console.log. captureStdout
|
|
547
|
+
// mocks both correctly. captureCommitOutput exists for historical reasons but
|
|
548
|
+
// doesn't actually intercept output() writes — its assertions silently no-op
|
|
549
|
+
// because process.exit kills the test worker before they run.
|
|
550
|
+
|
|
551
|
+
describe('cmdCommit populates dirty_after', () => {
|
|
552
|
+
let fixture;
|
|
553
|
+
let commands;
|
|
554
|
+
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
fixture = setupGitFixture('off');
|
|
557
|
+
commands = require('./commands.cjs');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
afterEach(() => {
|
|
561
|
+
fixture.cleanup();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('returns dirty_after=[] when tree is clean after commit', () => {
|
|
565
|
+
writeAndStageFile(fixture.cwd, 'clean-after.txt', 'clean');
|
|
566
|
+
const capture = captureStdout(() => {
|
|
567
|
+
commands.cmdCommit(fixture.cwd, 'test: clean dirty_after', [], false, false, false);
|
|
568
|
+
});
|
|
569
|
+
const result = capture.json;
|
|
570
|
+
assert.ok(result, 'expected JSON result. stdout: ' + capture.stdout);
|
|
571
|
+
assert.equal(result.committed, true);
|
|
572
|
+
assert.ok(result.hash);
|
|
573
|
+
assert.ok(Array.isArray(result.dirty_after), 'dirty_after should be an array');
|
|
574
|
+
assert.equal(result.dirty_after.length, 0, `clean tree should have empty dirty_after, got: ${JSON.stringify(result.dirty_after)}`);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('includes unstaged files in dirty_after after commit', () => {
|
|
578
|
+
// Stage and commit file A
|
|
579
|
+
writeAndStageFile(fixture.cwd, 'staged-file.txt', 'staged');
|
|
580
|
+
// Write (but do NOT stage) file B — it should remain dirty after the commit
|
|
581
|
+
fs.writeFileSync(path.join(fixture.cwd, 'dirty-file.txt'), 'unstaged');
|
|
582
|
+
|
|
583
|
+
const capture = captureStdout(() => {
|
|
584
|
+
commands.cmdCommit(fixture.cwd, 'test: dirty dirty_after', ['staged-file.txt'], false, false, false);
|
|
585
|
+
});
|
|
586
|
+
const result = capture.json;
|
|
587
|
+
assert.ok(result, 'expected JSON result. stdout: ' + capture.stdout);
|
|
588
|
+
assert.equal(result.committed, true);
|
|
589
|
+
assert.ok(result.hash);
|
|
590
|
+
assert.ok(Array.isArray(result.dirty_after), 'dirty_after should be an array');
|
|
591
|
+
assert.ok(
|
|
592
|
+
result.dirty_after.includes('dirty-file.txt'),
|
|
593
|
+
`dirty_after should include the unstaged file, got: ${JSON.stringify(result.dirty_after)}`
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('emits dirty_after in nothing_to_commit branch', () => {
|
|
598
|
+
// Create an unstaged file to make the tree dirty
|
|
599
|
+
fs.writeFileSync(path.join(fixture.cwd, 'unstaged-only.txt'), 'unstaged');
|
|
600
|
+
|
|
601
|
+
const capture = captureStdout(() => {
|
|
602
|
+
// Pass a pathspec that matches nothing so staging is a no-op — the
|
|
603
|
+
// unstaged file remains dirty AND there is nothing_to_commit.
|
|
604
|
+
commands.cmdCommit(fixture.cwd, 'test: nothing', ['nonexistent-pathspec-zzz'], false, false, false);
|
|
605
|
+
});
|
|
606
|
+
const result = capture.json;
|
|
607
|
+
assert.ok(result, 'expected JSON result. stdout: ' + capture.stdout);
|
|
608
|
+
assert.equal(result.committed, false, `expected committed=false, got result: ${JSON.stringify(result)}`);
|
|
609
|
+
assert.equal(result.reason, 'nothing_to_commit');
|
|
610
|
+
assert.ok(Array.isArray(result.dirty_after), 'dirty_after should be an array even when nothing_to_commit');
|
|
611
|
+
assert.ok(
|
|
612
|
+
result.dirty_after.includes('unstaged-only.txt'),
|
|
613
|
+
`dirty_after should include the unstaged file in nothing_to_commit branch, got: ${JSON.stringify(result.dirty_after)}`
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('runs porcelain in repoCwd when --repo-cwd is passed (not in cwd)', () => {
|
|
618
|
+
// Second fixture for the worktree
|
|
619
|
+
const worktreeFixture = setupGitFixture('off');
|
|
620
|
+
try {
|
|
621
|
+
// Write file in the worktree ONLY — planning root stays untouched
|
|
622
|
+
const stagedName = 'worktree-staged.txt';
|
|
623
|
+
fs.writeFileSync(path.join(worktreeFixture.cwd, stagedName), 'content');
|
|
624
|
+
execSync(`git add "${stagedName}"`, { cwd: worktreeFixture.cwd, stdio: 'pipe' });
|
|
625
|
+
// Dirty file in the worktree that is NOT staged
|
|
626
|
+
fs.writeFileSync(path.join(worktreeFixture.cwd, 'worktree-dirty.txt'), 'unstaged');
|
|
627
|
+
// Separately, make the PLANNING root dirty — this should NOT appear in dirty_after
|
|
628
|
+
fs.writeFileSync(path.join(fixture.cwd, 'planning-dirty.txt'), 'should-not-see-this');
|
|
629
|
+
|
|
630
|
+
const capture = captureStdout(() => {
|
|
631
|
+
commands.cmdCommit(
|
|
632
|
+
fixture.cwd, // cwd (planning root)
|
|
633
|
+
'test: repo-cwd dirty', // message
|
|
634
|
+
[stagedName], // files (relative to repoCwd)
|
|
635
|
+
false, // raw=false -> JSON output
|
|
636
|
+
false,
|
|
637
|
+
false,
|
|
638
|
+
worktreeFixture.cwd // repoCwd
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
const result = capture.json;
|
|
642
|
+
assert.ok(result, 'expected JSON result. stdout: ' + capture.stdout);
|
|
643
|
+
assert.equal(result.committed, true);
|
|
644
|
+
assert.ok(Array.isArray(result.dirty_after), 'dirty_after should be an array');
|
|
645
|
+
assert.ok(
|
|
646
|
+
result.dirty_after.includes('worktree-dirty.txt'),
|
|
647
|
+
`dirty_after should include the worktree-unstaged file, got: ${JSON.stringify(result.dirty_after)}`
|
|
648
|
+
);
|
|
649
|
+
assert.ok(
|
|
650
|
+
!result.dirty_after.includes('planning-dirty.txt'),
|
|
651
|
+
`dirty_after must NOT include planning-root files when --repo-cwd is set, got: ${JSON.stringify(result.dirty_after)}`
|
|
652
|
+
);
|
|
653
|
+
} finally {
|
|
654
|
+
worktreeFixture.cleanup();
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
544
659
|
// ─── TODO_STATUSES constant ─────────────────────────────────────────────────
|
|
545
660
|
|
|
546
661
|
describe('TODO_STATUSES constant', () => {
|