gsdd-cli 0.27.0 → 0.29.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/README.md +53 -22
- package/agents/DISTILLATION.md +2 -2
- package/agents/README.md +4 -4
- package/agents/approach-explorer.md +4 -4
- package/agents/executor.md +20 -20
- package/agents/integration-checker.md +2 -2
- package/agents/planner.md +10 -26
- package/agents/researcher.md +2 -2
- package/agents/roadmapper.md +6 -6
- package/agents/synthesizer.md +18 -18
- package/agents/verifier.md +3 -3
- package/bin/adapters/agents.mjs +3 -3
- package/bin/adapters/claude.mjs +16 -14
- package/bin/adapters/opencode.mjs +15 -12
- package/bin/gsdd.mjs +16 -13
- package/bin/lib/{models.mjs → config.mjs} +23 -16
- package/bin/lib/control-map.mjs +17 -488
- package/bin/lib/health-truth.mjs +8 -13
- package/bin/lib/health.mjs +25 -39
- package/bin/lib/init-flow.mjs +44 -38
- package/bin/lib/init-prompts.mjs +3 -3
- package/bin/lib/init-runtime.mjs +11 -30
- package/bin/lib/lifecycle-preflight.mjs +97 -410
- package/bin/lib/lifecycle-state.mjs +2 -1
- package/bin/lib/next.mjs +243 -20
- package/bin/lib/phase.mjs +706 -280
- package/bin/lib/plan-constants.mjs +0 -5
- package/bin/lib/rendering.mjs +64 -44
- package/bin/lib/runtime-freshness.mjs +18 -15
- package/bin/lib/state-dir.mjs +45 -0
- package/bin/lib/templates.mjs +59 -22
- package/bin/lib/work-context.mjs +12 -1
- package/bin/lib/workflows.mjs +0 -1
- package/bin/lib/workspace-root.mjs +11 -6
- package/distilled/DESIGN.md +89 -31
- package/distilled/EVIDENCE-INDEX.md +18 -5
- package/distilled/README.md +23 -33
- package/distilled/SKILL.md +9 -10
- package/distilled/templates/agents.block.md +5 -5
- package/distilled/templates/approach.md +3 -3
- package/distilled/templates/auth-matrix.md +2 -2
- package/distilled/templates/brownfield-change/CHANGE.md +1 -1
- package/distilled/templates/delegates/approach-explorer.md +5 -5
- package/distilled/templates/delegates/mapper-arch.md +3 -3
- package/distilled/templates/delegates/mapper-concerns.md +4 -4
- package/distilled/templates/delegates/mapper-quality.md +3 -3
- package/distilled/templates/delegates/mapper-tech.md +3 -3
- package/distilled/templates/delegates/plan-checker.md +18 -19
- package/distilled/templates/delegates/researcher-architecture.md +3 -3
- package/distilled/templates/delegates/researcher-features.md +3 -3
- package/distilled/templates/delegates/researcher-pitfalls.md +3 -3
- package/distilled/templates/delegates/researcher-stack.md +3 -3
- package/distilled/templates/delegates/researcher-synthesizer.md +7 -7
- package/distilled/templates/research/architecture.md +1 -1
- package/distilled/templates/research/pitfalls.md +1 -1
- package/distilled/templates/research/stack.md +1 -1
- package/distilled/templates/roadmap.md +4 -4
- package/distilled/templates/spec.md +2 -2
- package/distilled/templates/ui-proof.md +81 -181
- package/distilled/workflows/audit-milestone.md +23 -23
- package/distilled/workflows/complete-milestone.md +35 -35
- package/distilled/workflows/execute.md +34 -35
- package/distilled/workflows/map-codebase.md +30 -30
- package/distilled/workflows/new-milestone.md +18 -18
- package/distilled/workflows/new-project.md +45 -45
- package/distilled/workflows/pause.md +15 -15
- package/distilled/workflows/plan.md +106 -114
- package/distilled/workflows/progress.md +40 -39
- package/distilled/workflows/quick.md +49 -50
- package/distilled/workflows/resume.md +23 -22
- package/distilled/workflows/verify-work.md +7 -7
- package/distilled/workflows/verify.md +26 -26
- package/docs/BROWNFIELD-PROOF.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +13 -13
- package/docs/USER-GUIDE.md +26 -21
- package/docs/claude/context-monitor.md +1 -1
- package/docs/proof/consumer-node-cli/README.md +1 -1
- package/package.json +3 -3
- package/bin/lib/closeout-report.mjs +0 -318
- package/bin/lib/evidence-contract.mjs +0 -325
- package/bin/lib/provenance.mjs +0 -390
- package/bin/lib/session-fingerprint.mjs +0 -223
- package/bin/lib/ui-proof.mjs +0 -1007
- package/distilled/workflows/plan-milestone-gaps.md +0 -204
package/bin/lib/control-map.mjs
CHANGED
|
@@ -5,16 +5,12 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
6
|
import { output, parseFlagValue } from './cli-utils.mjs';
|
|
7
7
|
import { evaluateLifecycleState } from './lifecycle-state.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveStateDir } from './state-dir.mjs';
|
|
9
9
|
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
10
10
|
|
|
11
|
-
const DEFAULT_ANNOTATIONS_RELATIVE_PATH = '.planning/.local/control-map.annotations.json';
|
|
12
11
|
const MAX_DIRTY_BUCKET_ENTRIES = 200;
|
|
13
12
|
const CLEANUP_STATES = Object.freeze(['active', 'paused', 'merged', 'abandoned', 'superseded', 'cleanup_deferred']);
|
|
14
13
|
const ACTIVE_ANNOTATION_STATES = Object.freeze(['active', 'paused', 'cleanup_deferred']);
|
|
15
|
-
const CONTROL_MAP_USAGE = 'Usage: gsdd control-map [--json] [--with-ignored] [--annotations <path>]';
|
|
16
|
-
const CONTROL_MAP_ANNOTATE_SET_USAGE = 'Usage: gsdd control-map annotate set [--id <id>] [--path <worktree>] --write-set <paths> [--owner <runtime>] [--scope <text>] [--cleanup-state <state>] [--next-step <text>] [--refresh] [--annotations <path>]';
|
|
17
|
-
const CONTROL_MAP_ANNOTATE_CLEAR_USAGE = 'Usage: gsdd control-map annotate clear (--id <id> | --path <worktree>) [--annotations <path>]';
|
|
18
14
|
const AUTHORITY_ORDER = Object.freeze([
|
|
19
15
|
'repo_truth',
|
|
20
16
|
'planning_artifacts',
|
|
@@ -241,7 +237,7 @@ function findDirtyWriteSetOverlaps(writeEntries, dirtyEntries) {
|
|
|
241
237
|
if (!pathsIntersect(writeEntry.repo_path, dirtyEntry.repo_path)) continue;
|
|
242
238
|
overlaps.push({
|
|
243
239
|
annotation_id: writeEntry.annotation_id,
|
|
244
|
-
|
|
240
|
+
classified_worktree_id: writeEntry.worktree_id,
|
|
245
241
|
write_path: writeEntry.repo_path,
|
|
246
242
|
dirty_worktree_id: dirtyEntry.worktree_id,
|
|
247
243
|
dirty_path: dirtyEntry.repo_path,
|
|
@@ -497,383 +493,6 @@ function loadAnnotations(workspaceRoot, planningDir, annotationPathArg = null) {
|
|
|
497
493
|
};
|
|
498
494
|
}
|
|
499
495
|
|
|
500
|
-
function parseAnnotationMutationFlags(args, usage, { valueFlags, booleanFlags = [] }) {
|
|
501
|
-
const allowedValueFlags = new Set(valueFlags);
|
|
502
|
-
const allowedBooleanFlags = new Set(booleanFlags);
|
|
503
|
-
const values = new Map();
|
|
504
|
-
const listValues = new Map([['--write-set', []]]);
|
|
505
|
-
const booleans = new Set();
|
|
506
|
-
const unknown = [];
|
|
507
|
-
|
|
508
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
509
|
-
const arg = args[index];
|
|
510
|
-
if (allowedBooleanFlags.has(arg)) {
|
|
511
|
-
booleans.add(arg);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (!allowedValueFlags.has(arg)) {
|
|
515
|
-
unknown.push(arg);
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const value = args[index + 1];
|
|
520
|
-
if (!value || value.startsWith('--')) {
|
|
521
|
-
return { valid: false, usage, error: `Missing value for ${arg}.` };
|
|
522
|
-
}
|
|
523
|
-
if (arg === '--write-set') listValues.get(arg).push(value);
|
|
524
|
-
else values.set(arg, value);
|
|
525
|
-
index += 1;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (unknown.length > 0) {
|
|
529
|
-
return { valid: false, usage, error: `Unsupported argument(s): ${unknown.join(', ')}` };
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return {
|
|
533
|
-
valid: true,
|
|
534
|
-
values,
|
|
535
|
-
listValues,
|
|
536
|
-
booleans,
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function parseWriteSet(values, worktreePath) {
|
|
541
|
-
const entries = [];
|
|
542
|
-
for (const rawValue of values || []) {
|
|
543
|
-
for (const part of String(rawValue).split(',')) {
|
|
544
|
-
const normalized = normalizeRepoPath(part, worktreePath);
|
|
545
|
-
if (normalized && !entries.includes(normalized)) entries.push(normalized);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return entries;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function annotationPathLabelForWrite(workspaceRoot, worktreePath) {
|
|
552
|
-
return workspacePathLabel(workspaceRoot, worktreePath);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function annotationIdForWorktree(worktree) {
|
|
556
|
-
if (worktree.id === '.') return 'canonical';
|
|
557
|
-
return worktree.id;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function serializeAnnotationEntry(workspaceRoot, entry) {
|
|
561
|
-
const serialized = {
|
|
562
|
-
id: entry.id,
|
|
563
|
-
path: entry.path || annotationPathLabelForWrite(workspaceRoot, entry.normalized_path),
|
|
564
|
-
cleanup_state: entry.cleanup_state || 'active',
|
|
565
|
-
};
|
|
566
|
-
if (entry.runtime_owner) serialized.runtime_owner = entry.runtime_owner;
|
|
567
|
-
if (entry.branch) serialized.branch = entry.branch;
|
|
568
|
-
if (entry.last_known_head) serialized.last_known_head = entry.last_known_head;
|
|
569
|
-
if (entry.intended_scope) serialized.intended_scope = entry.intended_scope;
|
|
570
|
-
if (Array.isArray(entry.write_set) && entry.write_set.length > 0) serialized.write_set = entry.write_set;
|
|
571
|
-
if (entry.next_step) serialized.next_step = entry.next_step;
|
|
572
|
-
if (entry.updated_at) serialized.updated_at = entry.updated_at;
|
|
573
|
-
return serialized;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function writeAnnotationsDocument(workspaceRoot, annotationFile, worktrees) {
|
|
577
|
-
mkdirSync(dirname(annotationFile.path), { recursive: true });
|
|
578
|
-
const document = {
|
|
579
|
-
schema_version: 1,
|
|
580
|
-
worktrees: worktrees.map((entry) => serializeAnnotationEntry(workspaceRoot, entry)),
|
|
581
|
-
};
|
|
582
|
-
writeFileSync(annotationFile.path, `${JSON.stringify(document, null, 2)}\n`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function buildMutationContext(context, annotationPathArg) {
|
|
586
|
-
const annotationFile = resolveAnnotationFilePath(context.workspaceRoot, context.planningDir, annotationPathArg);
|
|
587
|
-
if (!annotationFile.inside_workspace) {
|
|
588
|
-
return {
|
|
589
|
-
ok: false,
|
|
590
|
-
result: {
|
|
591
|
-
status: 'blocked',
|
|
592
|
-
reason: 'annotations_path_outside_workspace',
|
|
593
|
-
annotations_path: annotationFile.label,
|
|
594
|
-
message: 'Control-map annotations path must stay inside the workspace.',
|
|
595
|
-
},
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const annotations = loadAnnotations(context.workspaceRoot, context.planningDir, annotationPathArg);
|
|
600
|
-
if (!annotations.valid) {
|
|
601
|
-
return {
|
|
602
|
-
ok: false,
|
|
603
|
-
result: {
|
|
604
|
-
status: 'blocked',
|
|
605
|
-
reason: 'invalid_annotations',
|
|
606
|
-
annotations_path: annotations.path,
|
|
607
|
-
errors: annotations.errors,
|
|
608
|
-
},
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const discovered = discoverGitWorktrees(context.workspaceRoot);
|
|
613
|
-
return {
|
|
614
|
-
ok: true,
|
|
615
|
-
annotationFile,
|
|
616
|
-
annotations,
|
|
617
|
-
worktrees: discovered.worktrees,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function findWorktreeByPath(worktrees, pathValue) {
|
|
622
|
-
const normalized = normalizeSlashes(resolve(pathValue));
|
|
623
|
-
return worktrees.find((worktree) => normalizeSlashes(resolve(worktree.path)) === normalized) || null;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function findAnnotationIndex(annotations, { id = null, normalizedPath = null } = {}) {
|
|
627
|
-
if (id) {
|
|
628
|
-
const byId = annotations.worktrees.findIndex((entry) => entry.id === id);
|
|
629
|
-
if (byId !== -1) return byId;
|
|
630
|
-
}
|
|
631
|
-
if (normalizedPath) {
|
|
632
|
-
return annotations.worktrees.findIndex((entry) => (
|
|
633
|
-
entry.normalized_path && normalizeSlashes(resolve(entry.normalized_path)) === normalizeSlashes(resolve(normalizedPath))
|
|
634
|
-
));
|
|
635
|
-
}
|
|
636
|
-
return -1;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function staleAnnotationIssues(annotation, worktree) {
|
|
640
|
-
const issues = [];
|
|
641
|
-
if (annotation.branch && worktree.branch && annotation.branch !== worktree.branch) {
|
|
642
|
-
issues.push({
|
|
643
|
-
code: 'branch_mismatch',
|
|
644
|
-
saved: annotation.branch,
|
|
645
|
-
live: worktree.branch,
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
if (annotation.last_known_head && worktree.head && annotation.last_known_head !== worktree.head) {
|
|
649
|
-
issues.push({
|
|
650
|
-
code: 'head_mismatch',
|
|
651
|
-
saved: annotation.last_known_head,
|
|
652
|
-
live: worktree.head,
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
return issues;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function annotationMutationSummary(annotation) {
|
|
659
|
-
return {
|
|
660
|
-
id: annotation.id,
|
|
661
|
-
path: annotation.path,
|
|
662
|
-
runtime_owner: annotation.runtime_owner || null,
|
|
663
|
-
branch: annotation.branch || null,
|
|
664
|
-
last_known_head: annotation.last_known_head || null,
|
|
665
|
-
intended_scope: annotation.intended_scope || null,
|
|
666
|
-
write_set: annotation.write_set || [],
|
|
667
|
-
cleanup_state: annotation.cleanup_state || 'active',
|
|
668
|
-
next_step: annotation.next_step || null,
|
|
669
|
-
updated_at: annotation.updated_at || null,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function setAnnotation(context, args) {
|
|
674
|
-
const parsed = parseAnnotationMutationFlags(args, CONTROL_MAP_ANNOTATE_SET_USAGE, {
|
|
675
|
-
valueFlags: [
|
|
676
|
-
'--annotations',
|
|
677
|
-
'--cleanup-state',
|
|
678
|
-
'--id',
|
|
679
|
-
'--next-step',
|
|
680
|
-
'--owner',
|
|
681
|
-
'--path',
|
|
682
|
-
'--runtime-owner',
|
|
683
|
-
'--scope',
|
|
684
|
-
'--write-set',
|
|
685
|
-
],
|
|
686
|
-
booleanFlags: ['--refresh'],
|
|
687
|
-
});
|
|
688
|
-
if (!parsed.valid) return { ok: false, result: { status: 'blocked', reason: 'invalid_arguments', message: parsed.error, usage: parsed.usage } };
|
|
689
|
-
|
|
690
|
-
const annotationPathArg = parsed.values.get('--annotations') || null;
|
|
691
|
-
const mutation = buildMutationContext(context, annotationPathArg);
|
|
692
|
-
if (!mutation.ok) return mutation;
|
|
693
|
-
|
|
694
|
-
const explicitId = parsed.values.get('--id') || null;
|
|
695
|
-
const pathValue = parsed.values.get('--path') || '.';
|
|
696
|
-
const targetPath = resolve(context.workspaceRoot, pathValue);
|
|
697
|
-
const worktree = findWorktreeByPath(mutation.worktrees, targetPath);
|
|
698
|
-
if (!worktree || !worktree.git_valid) {
|
|
699
|
-
return {
|
|
700
|
-
ok: false,
|
|
701
|
-
result: {
|
|
702
|
-
status: 'blocked',
|
|
703
|
-
reason: 'worktree_not_found',
|
|
704
|
-
message: `No live git worktree found for ${pathValue}.`,
|
|
705
|
-
path: workspacePathLabel(context.workspaceRoot, targetPath),
|
|
706
|
-
},
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
const writeSet = parseWriteSet(parsed.listValues.get('--write-set'), worktree.path);
|
|
711
|
-
if (writeSet.length === 0) {
|
|
712
|
-
return {
|
|
713
|
-
ok: false,
|
|
714
|
-
result: {
|
|
715
|
-
status: 'blocked',
|
|
716
|
-
reason: 'missing_write_set',
|
|
717
|
-
message: 'Annotation set requires at least one --write-set value.',
|
|
718
|
-
usage: CONTROL_MAP_ANNOTATE_SET_USAGE,
|
|
719
|
-
},
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const normalizedPath = normalizeSlashes(resolve(worktree.path));
|
|
724
|
-
const existingIndex = findAnnotationIndex(mutation.annotations, { id: explicitId, normalizedPath });
|
|
725
|
-
const existing = existingIndex === -1 ? null : mutation.annotations.worktrees[existingIndex];
|
|
726
|
-
|
|
727
|
-
const cleanupState = parsed.values.get('--cleanup-state') || existing?.cleanup_state || 'active';
|
|
728
|
-
if (!CLEANUP_STATES.includes(cleanupState)) {
|
|
729
|
-
return {
|
|
730
|
-
ok: false,
|
|
731
|
-
result: {
|
|
732
|
-
status: 'blocked',
|
|
733
|
-
reason: 'invalid_cleanup_state',
|
|
734
|
-
message: `Unsupported cleanup_state: ${cleanupState}`,
|
|
735
|
-
supported_cleanup_states: CLEANUP_STATES,
|
|
736
|
-
},
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const staleIssues = existing ? staleAnnotationIssues(existing, worktree) : [];
|
|
741
|
-
const refresh = parsed.booleans.has('--refresh');
|
|
742
|
-
if (staleIssues.length > 0 && !refresh) {
|
|
743
|
-
return {
|
|
744
|
-
ok: false,
|
|
745
|
-
result: {
|
|
746
|
-
operation: 'control-map annotate set',
|
|
747
|
-
status: 'blocked',
|
|
748
|
-
reason: 'stale_annotation',
|
|
749
|
-
annotations_path: mutation.annotationFile.label,
|
|
750
|
-
annotation_id: existing.id,
|
|
751
|
-
stale_issues: staleIssues,
|
|
752
|
-
message: 'Existing annotation is stale against live worktree truth; rerun with --refresh to update it or use annotate clear to remove it.',
|
|
753
|
-
},
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const now = new Date().toISOString();
|
|
758
|
-
const annotation = {
|
|
759
|
-
id: explicitId || existing?.id || annotationIdForWorktree(worktree),
|
|
760
|
-
path: annotationPathLabelForWrite(context.workspaceRoot, worktree.path),
|
|
761
|
-
runtime_owner: parsed.values.get('--runtime-owner') || parsed.values.get('--owner') || existing?.runtime_owner || null,
|
|
762
|
-
branch: worktree.branch,
|
|
763
|
-
last_known_head: worktree.head,
|
|
764
|
-
intended_scope: parsed.values.get('--scope') || existing?.intended_scope || null,
|
|
765
|
-
write_set: writeSet,
|
|
766
|
-
cleanup_state: cleanupState,
|
|
767
|
-
next_step: parsed.values.get('--next-step') || existing?.next_step || null,
|
|
768
|
-
updated_at: now,
|
|
769
|
-
};
|
|
770
|
-
const nextWorktrees = mutation.annotations.worktrees.slice();
|
|
771
|
-
if (existingIndex === -1) nextWorktrees.push(annotation);
|
|
772
|
-
else nextWorktrees[existingIndex] = annotation;
|
|
773
|
-
writeAnnotationsDocument(context.workspaceRoot, mutation.annotationFile, nextWorktrees);
|
|
774
|
-
|
|
775
|
-
return {
|
|
776
|
-
ok: true,
|
|
777
|
-
result: {
|
|
778
|
-
operation: 'control-map annotate set',
|
|
779
|
-
changed: true,
|
|
780
|
-
status: existing ? (refresh && staleIssues.length > 0 ? 'refreshed' : 'updated') : 'created',
|
|
781
|
-
annotations_path: mutation.annotationFile.label,
|
|
782
|
-
annotation: annotationMutationSummary(annotation),
|
|
783
|
-
stale_check: {
|
|
784
|
-
status: staleIssues.length > 0 ? 'refreshed' : 'passed',
|
|
785
|
-
issues: staleIssues,
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function clearAnnotation(context, args) {
|
|
792
|
-
const parsed = parseAnnotationMutationFlags(args, CONTROL_MAP_ANNOTATE_CLEAR_USAGE, {
|
|
793
|
-
valueFlags: ['--annotations', '--id', '--path'],
|
|
794
|
-
});
|
|
795
|
-
if (!parsed.valid) return { ok: false, result: { status: 'blocked', reason: 'invalid_arguments', message: parsed.error, usage: parsed.usage } };
|
|
796
|
-
|
|
797
|
-
const id = parsed.values.get('--id') || null;
|
|
798
|
-
const pathValue = parsed.values.get('--path') || null;
|
|
799
|
-
if (!id && !pathValue) {
|
|
800
|
-
return {
|
|
801
|
-
ok: false,
|
|
802
|
-
result: {
|
|
803
|
-
status: 'blocked',
|
|
804
|
-
reason: 'missing_selector',
|
|
805
|
-
message: 'Annotation clear requires --id or --path.',
|
|
806
|
-
usage: CONTROL_MAP_ANNOTATE_CLEAR_USAGE,
|
|
807
|
-
},
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const annotationPathArg = parsed.values.get('--annotations') || null;
|
|
812
|
-
const mutation = buildMutationContext(context, annotationPathArg);
|
|
813
|
-
if (!mutation.ok) return mutation;
|
|
814
|
-
if (!mutation.annotations.exists) {
|
|
815
|
-
return {
|
|
816
|
-
ok: true,
|
|
817
|
-
result: {
|
|
818
|
-
operation: 'control-map annotate clear',
|
|
819
|
-
changed: false,
|
|
820
|
-
status: 'missing',
|
|
821
|
-
annotations_path: mutation.annotationFile.label,
|
|
822
|
-
},
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
const normalizedPath = pathValue ? normalizeSlashes(resolve(context.workspaceRoot, pathValue)) : null;
|
|
827
|
-
const existingIndex = findAnnotationIndex(mutation.annotations, { id, normalizedPath });
|
|
828
|
-
if (existingIndex === -1) {
|
|
829
|
-
return {
|
|
830
|
-
ok: true,
|
|
831
|
-
result: {
|
|
832
|
-
operation: 'control-map annotate clear',
|
|
833
|
-
changed: false,
|
|
834
|
-
status: 'not_found',
|
|
835
|
-
annotations_path: mutation.annotationFile.label,
|
|
836
|
-
selector: id ? { id } : { path: workspacePathLabel(context.workspaceRoot, normalizedPath) },
|
|
837
|
-
},
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const removed = mutation.annotations.worktrees[existingIndex];
|
|
842
|
-
const nextWorktrees = mutation.annotations.worktrees.filter((_, index) => index !== existingIndex);
|
|
843
|
-
writeAnnotationsDocument(context.workspaceRoot, mutation.annotationFile, nextWorktrees);
|
|
844
|
-
|
|
845
|
-
return {
|
|
846
|
-
ok: true,
|
|
847
|
-
result: {
|
|
848
|
-
operation: 'control-map annotate clear',
|
|
849
|
-
changed: true,
|
|
850
|
-
status: 'cleared',
|
|
851
|
-
annotations_path: mutation.annotationFile.label,
|
|
852
|
-
annotation: annotationMutationSummary(removed),
|
|
853
|
-
},
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function cmdControlMapAnnotate(context, args) {
|
|
858
|
-
const [operation, ...operationArgs] = args;
|
|
859
|
-
let result;
|
|
860
|
-
if (operation === 'set') result = setAnnotation(context, operationArgs);
|
|
861
|
-
else if (operation === 'clear') result = clearAnnotation(context, operationArgs);
|
|
862
|
-
else {
|
|
863
|
-
result = {
|
|
864
|
-
ok: false,
|
|
865
|
-
result: {
|
|
866
|
-
status: 'blocked',
|
|
867
|
-
reason: 'invalid_operation',
|
|
868
|
-
message: 'Usage: gsdd control-map annotate <set|clear> ...',
|
|
869
|
-
},
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
output(result.result);
|
|
874
|
-
if (!result.ok) process.exitCode = 1;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
496
|
function reconcileAnnotations(worktrees, annotations) {
|
|
878
497
|
const warnings = [];
|
|
879
498
|
const byPath = new Map(worktrees.map((entry) => [normalizeSlashes(resolve(entry.path)), entry]));
|
|
@@ -923,7 +542,7 @@ function reconcileAnnotations(worktrees, annotations) {
|
|
|
923
542
|
function classifyWorkflowState(planningDir) {
|
|
924
543
|
const lifecycle = evaluateLifecycleState({ planningDir });
|
|
925
544
|
const checkpointPath = join(planningDir, '.continue-here.md');
|
|
926
|
-
const
|
|
545
|
+
const workspaceRoot = resolve(planningDir, '..');
|
|
927
546
|
const milestoneVersion = lifecycle.currentMilestone?.version || null;
|
|
928
547
|
const milestoneTitle = lifecycle.currentMilestone?.title || null;
|
|
929
548
|
const milestone = milestoneVersion || milestoneTitle
|
|
@@ -936,11 +555,7 @@ function classifyWorkflowState(planningDir) {
|
|
|
936
555
|
non_phase_state: lifecycle.nonPhaseState || null,
|
|
937
556
|
checkpoint: {
|
|
938
557
|
exists: existsSync(checkpointPath),
|
|
939
|
-
path:
|
|
940
|
-
},
|
|
941
|
-
planning_drift: {
|
|
942
|
-
drifted: drift.drifted,
|
|
943
|
-
details: drift.details || [],
|
|
558
|
+
path: workspacePathLabel(workspaceRoot, checkpointPath),
|
|
944
559
|
},
|
|
945
560
|
};
|
|
946
561
|
}
|
|
@@ -975,7 +590,7 @@ function addBranchStateRisks(risks, worktree, { canonical = false } = {}) {
|
|
|
975
590
|
}
|
|
976
591
|
}
|
|
977
592
|
|
|
978
|
-
function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtimeDirs, workflowState, gitErrors }) {
|
|
593
|
+
function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtimeDirs, workflowState, gitErrors, helperCommand }) {
|
|
979
594
|
const risks = [];
|
|
980
595
|
const writeEntries = annotationWriteEntries(worktrees, rawAnnotations || { worktrees: [] });
|
|
981
596
|
const dirtyEntries = worktrees.flatMap((worktree) => dirtyRepoPathEntries(worktree));
|
|
@@ -988,7 +603,7 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
988
603
|
case 'canonical_git_invalid':
|
|
989
604
|
case 'worktree_git_invalid': {
|
|
990
605
|
const targetPath = risk.worktree_id || canonical.path;
|
|
991
|
-
return `Run \`git config --global --add safe.directory ${targetPath}\`, then re-run
|
|
606
|
+
return `Run \`git config --global --add safe.directory ${targetPath}\`, then re-run \`${helperCommand}\`.`;
|
|
992
607
|
}
|
|
993
608
|
case 'canonical_dirty':
|
|
994
609
|
return 'Commit, stash, or checkpoint the canonical changes before planning, cleanup, merge, or broad execution.';
|
|
@@ -1002,14 +617,12 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
1002
617
|
case 'detached_candidate_worktree':
|
|
1003
618
|
return 'Classify the detached worktree intent (active vs abandoned) before using it for execution or cleanup decisions.';
|
|
1004
619
|
case 'sibling_worktree_dirty':
|
|
1005
|
-
case '
|
|
620
|
+
case 'unclassified_candidate_worktree':
|
|
1006
621
|
return 'Review sibling worktree ownership and write set before starting overlapping implementation.';
|
|
1007
622
|
case 'write_set_overlap':
|
|
1008
623
|
return 'Resolve overlapping local annotation write sets before starting another owned-write workflow.';
|
|
1009
624
|
case 'dirty_path_write_set_overlap':
|
|
1010
|
-
return 'Checkpoint or classify dirty paths that overlap
|
|
1011
|
-
case 'planning_state_drift':
|
|
1012
|
-
return 'Review drift and rebaseline with session-fingerprint only after confirming the planning changes are intentional.';
|
|
625
|
+
return 'Checkpoint or classify dirty paths that overlap classified write sets before owned-write transitions.';
|
|
1013
626
|
default:
|
|
1014
627
|
return null;
|
|
1015
628
|
}
|
|
@@ -1091,7 +704,7 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
1091
704
|
}
|
|
1092
705
|
if (!worktree.annotation && (worktree.dirty.counts.tracked > 0 || worktree.dirty.counts.untracked > 0 || worktree.detached)) {
|
|
1093
706
|
const risk = {
|
|
1094
|
-
code: '
|
|
707
|
+
code: 'unclassified_candidate_worktree',
|
|
1095
708
|
severity: 'info',
|
|
1096
709
|
worktree_id: worktree.id,
|
|
1097
710
|
message: `Worktree ${worktree.id} has candidate-work signals but no local control-map annotation.`,
|
|
@@ -1115,7 +728,7 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
1115
728
|
const risk = {
|
|
1116
729
|
code: 'dirty_path_write_set_overlap',
|
|
1117
730
|
severity: 'block',
|
|
1118
|
-
message: `Live dirty paths overlap
|
|
731
|
+
message: `Live dirty paths overlap classified write sets (${dirtyWriteSetOverlaps.length} overlap(s)).`,
|
|
1119
732
|
overlaps: dirtyWriteSetOverlaps.slice(0, MAX_DIRTY_BUCKET_ENTRIES),
|
|
1120
733
|
omitted_count: Math.max(0, dirtyWriteSetOverlaps.length - MAX_DIRTY_BUCKET_ENTRIES),
|
|
1121
734
|
};
|
|
@@ -1134,16 +747,6 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
|
|
|
1134
747
|
path: runtimeDir.path_relative,
|
|
1135
748
|
});
|
|
1136
749
|
}
|
|
1137
|
-
if (workflowState.planning_drift.drifted) {
|
|
1138
|
-
const risk = {
|
|
1139
|
-
code: 'planning_state_drift',
|
|
1140
|
-
severity: 'warn',
|
|
1141
|
-
message: `Planning state drifted since the last fingerprint: ${workflowState.planning_drift.details.join('; ')}`,
|
|
1142
|
-
};
|
|
1143
|
-
risk.fix_hint = fixHintForRisk(risk);
|
|
1144
|
-
risks.push(risk);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
750
|
// Ensure the common closure risks expose actionable fix guidance even when
|
|
1148
751
|
// the originating helper (for example branch-state risks) didn't attach it.
|
|
1149
752
|
for (const risk of risks) {
|
|
@@ -1160,14 +763,13 @@ function buildInterventions(risks) {
|
|
|
1160
763
|
const interventions = [];
|
|
1161
764
|
if (codes.has('canonical_git_invalid')) interventions.push('Fix git/safe.directory access before mutating this checkout.');
|
|
1162
765
|
if (codes.has('write_set_overlap')) interventions.push('Resolve overlapping local annotation write sets before starting another owned-write workflow.');
|
|
1163
|
-
if (codes.has('dirty_path_write_set_overlap')) interventions.push('Checkpoint, commit, stash, or explicitly classify dirty paths that overlap
|
|
766
|
+
if (codes.has('dirty_path_write_set_overlap')) interventions.push('Checkpoint, commit, stash, or explicitly classify dirty paths that overlap classified write sets before owned-write transitions.');
|
|
1164
767
|
if (codes.has('canonical_dirty_behind_upstream')) interventions.push('Sync the canonical branch or preserve dirty canonical work before mutating a stale checkout.');
|
|
1165
768
|
if (codes.has('canonical_branch_behind_upstream') || codes.has('canonical_branch_diverged_upstream') || codes.has('worktree_branch_behind_upstream') || codes.has('worktree_branch_diverged_upstream')) interventions.push('Review upstream divergence before relying on stale branch state for implementation decisions.');
|
|
1166
769
|
if (codes.has('detached_candidate_worktree')) interventions.push('Classify detached worktree intent before using it for execution or cleanup decisions.');
|
|
1167
770
|
if (codes.has('canonical_dirty')) interventions.push('Checkpoint or classify canonical dirty work before planning, cleanup, merge, or broad execution.');
|
|
1168
|
-
if (codes.has('sibling_worktree_dirty') || codes.has('
|
|
771
|
+
if (codes.has('sibling_worktree_dirty') || codes.has('unclassified_candidate_worktree')) interventions.push('Review sibling worktree ownership and write set before starting overlapping implementation.');
|
|
1169
772
|
if (codes.has('stale_annotation_missing_worktree') || codes.has('stale_annotation_branch_mismatch') || codes.has('stale_annotation_head_mismatch')) interventions.push('Refresh or remove stale local control-map annotations after reviewing repo truth.');
|
|
1170
|
-
if (codes.has('planning_state_drift')) interventions.push('Review planning drift and rebaseline with session-fingerprint only after confirming the changes are intentional.');
|
|
1171
773
|
if (interventions.length === 0) interventions.push('No control-map intervention required before read-only status work.');
|
|
1172
774
|
return interventions;
|
|
1173
775
|
}
|
|
@@ -1180,7 +782,9 @@ export function buildControlMap({
|
|
|
1180
782
|
now = new Date(),
|
|
1181
783
|
} = {}) {
|
|
1182
784
|
const root = resolve(workspaceRoot || process.cwd());
|
|
1183
|
-
const planning = planningDir ||
|
|
785
|
+
const planning = planningDir || resolveStateDir(root).dir;
|
|
786
|
+
const defaultAnnotationsPath = resolveAnnotationFilePath(root, planning).label;
|
|
787
|
+
const helperCommand = `node ${workspacePathLabel(root, join(planning, 'bin', 'gsdd.mjs'))} control-map --json`;
|
|
1184
788
|
const discovered = discoverGitWorktrees(root, { includeIgnoredPaths, includeIgnoredCount: includeIgnoredPaths });
|
|
1185
789
|
const annotations = loadAnnotations(root, planning, annotationPath);
|
|
1186
790
|
const reconciled = reconcileAnnotations(discovered.worktrees, annotations);
|
|
@@ -1204,6 +808,7 @@ export function buildControlMap({
|
|
|
1204
808
|
runtimeDirs,
|
|
1205
809
|
workflowState,
|
|
1206
810
|
gitErrors: discovered.errors,
|
|
811
|
+
helperCommand,
|
|
1207
812
|
});
|
|
1208
813
|
|
|
1209
814
|
return {
|
|
@@ -1212,7 +817,7 @@ export function buildControlMap({
|
|
|
1212
817
|
operation: 'control-map',
|
|
1213
818
|
repo_root_id: canonical.git_top_level || normalizeSlashes(root),
|
|
1214
819
|
workspace_root: normalizeSlashes(root),
|
|
1215
|
-
default_annotations_path:
|
|
820
|
+
default_annotations_path: defaultAnnotationsPath,
|
|
1216
821
|
authority: AUTHORITY_ORDER,
|
|
1217
822
|
canonical_worktree: canonical,
|
|
1218
823
|
worktrees: reconciled.worktrees,
|
|
@@ -1223,79 +828,3 @@ export function buildControlMap({
|
|
|
1223
828
|
interventions: buildInterventions(risks),
|
|
1224
829
|
};
|
|
1225
830
|
}
|
|
1226
|
-
|
|
1227
|
-
function printHuman(map) {
|
|
1228
|
-
const milestone = map.workflow_state.current_milestone
|
|
1229
|
-
? [map.workflow_state.current_milestone.version, map.workflow_state.current_milestone.title].filter(Boolean).join(' ')
|
|
1230
|
-
: 'none';
|
|
1231
|
-
const ignoredLabel = map.canonical_worktree.dirty.ignored_count_included
|
|
1232
|
-
? String(map.canonical_worktree.dirty.counts.ignored)
|
|
1233
|
-
: 'not scanned';
|
|
1234
|
-
console.log('gsdd control-map - computed workspace control map\n');
|
|
1235
|
-
console.log(`Workspace: ${map.workspace_root}`);
|
|
1236
|
-
console.log(`Authority: ${map.authority.join(' > ')}`);
|
|
1237
|
-
console.log(`Canonical: ${map.canonical_worktree.branch || 'unknown'} @ ${map.canonical_worktree.head || 'unknown'}`);
|
|
1238
|
-
console.log(`Workflow: milestone=${milestone}, phase=${map.workflow_state.current_phase || 'none'}, next=${map.workflow_state.next_phase || 'none'}`);
|
|
1239
|
-
console.log(`Checkpoint: ${map.workflow_state.checkpoint.path} (${map.workflow_state.checkpoint.exists ? 'present' : 'missing'})`);
|
|
1240
|
-
console.log(`Dirty buckets: ${map.canonical_worktree.dirty.counts.tracked} tracked, ${map.canonical_worktree.dirty.counts.untracked} untracked, ${ignoredLabel} ignored`);
|
|
1241
|
-
console.log(`Worktrees: ${map.worktrees.length}`);
|
|
1242
|
-
for (const worktree of map.worktrees) {
|
|
1243
|
-
const marker = worktree.path === map.canonical_worktree.path ? '*' : '-';
|
|
1244
|
-
const annotation = worktree.annotation ? ` owner=${worktree.annotation.runtime_owner || 'unspecified'} cleanup=${worktree.annotation.cleanup_state}` : '';
|
|
1245
|
-
const ignoredCount = worktree.dirty.ignored_count_included ? String(worktree.dirty.counts.ignored) : 'not-scanned';
|
|
1246
|
-
console.log(` ${marker} ${worktree.id} branch=${worktree.branch || 'unknown'} dirty=${worktree.dirty.counts.tracked}/${worktree.dirty.counts.untracked}/${ignoredCount}${annotation}`);
|
|
1247
|
-
}
|
|
1248
|
-
if (map.risks.length > 0) {
|
|
1249
|
-
console.log('\nRisks:');
|
|
1250
|
-
for (const risk of map.risks) {
|
|
1251
|
-
console.log(` - [${risk.severity || 'info'}] ${risk.code}: ${risk.message}`);
|
|
1252
|
-
if (risk.fix_hint) console.log(` Fix: ${risk.fix_hint}`);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
console.log('\nInterventions:');
|
|
1256
|
-
for (const intervention of map.interventions) console.log(` - ${intervention}`);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
export function cmdControlMap(...args) {
|
|
1260
|
-
const jsonMode = args.includes('--json');
|
|
1261
|
-
const contextArgs = args.filter((arg) => arg !== '--json');
|
|
1262
|
-
const context = resolveWorkspaceContext(contextArgs);
|
|
1263
|
-
if (context.invalid) {
|
|
1264
|
-
console.error(context.error);
|
|
1265
|
-
process.exitCode = 1;
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (context.args[0] === 'annotate') {
|
|
1270
|
-
cmdControlMapAnnotate(context, context.args.slice(1));
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
const annotations = parseFlagValue(context.args, '--annotations');
|
|
1275
|
-
if (annotations.invalid) {
|
|
1276
|
-
console.error(CONTROL_MAP_USAGE);
|
|
1277
|
-
process.exitCode = 1;
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
const filteredArgs = context.args.filter((arg, index) => {
|
|
1281
|
-
if (arg === '--annotations') return false;
|
|
1282
|
-
if (index > 0 && context.args[index - 1] === '--annotations') return false;
|
|
1283
|
-
if (arg === '--with-ignored') return false;
|
|
1284
|
-
return true;
|
|
1285
|
-
});
|
|
1286
|
-
if (filteredArgs.length > 0) {
|
|
1287
|
-
console.error(CONTROL_MAP_USAGE);
|
|
1288
|
-
process.exitCode = 1;
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
const includeIgnoredPaths = context.args.includes('--with-ignored');
|
|
1293
|
-
const map = buildControlMap({
|
|
1294
|
-
workspaceRoot: context.workspaceRoot,
|
|
1295
|
-
planningDir: context.planningDir,
|
|
1296
|
-
annotationPath: annotations.value,
|
|
1297
|
-
includeIgnoredPaths,
|
|
1298
|
-
});
|
|
1299
|
-
if (jsonMode) output(map);
|
|
1300
|
-
else printHuman(map);
|
|
1301
|
-
}
|
package/bin/lib/health-truth.mjs
CHANGED
|
@@ -5,11 +5,15 @@ import {
|
|
|
5
5
|
getRuntimeFreshnessRepairGuidance,
|
|
6
6
|
summarizeRuntimeFreshnessIssues,
|
|
7
7
|
} from './runtime-freshness.mjs';
|
|
8
|
-
import { checkDrift } from './session-fingerprint.mjs';
|
|
9
8
|
|
|
10
|
-
export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10', 'W11'
|
|
9
|
+
export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10', 'W11'];
|
|
10
|
+
|
|
11
|
+
function statePath(stateDirName, relativePath = '') {
|
|
12
|
+
return relativePath ? `${stateDirName}/${relativePath}` : stateDirName;
|
|
13
|
+
}
|
|
11
14
|
|
|
12
15
|
export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, options = {}) {
|
|
16
|
+
const stateDirName = options.stateDirName || '.work';
|
|
13
17
|
const warnings = [];
|
|
14
18
|
const designPath = join(frameworkDir, 'distilled', 'DESIGN.md');
|
|
15
19
|
const readmePath = join(frameworkDir, 'distilled', 'README.md');
|
|
@@ -85,7 +89,7 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
|
|
|
85
89
|
id: 'W10',
|
|
86
90
|
severity: 'WARN',
|
|
87
91
|
message: `ROADMAP/SPEC requirement status drift (${mismatches.join('; ')})`,
|
|
88
|
-
fix:
|
|
92
|
+
fix: `Reconcile ${statePath(stateDirName, 'ROADMAP.md')} phase completion markers with ${statePath(stateDirName, 'SPEC.md')} requirement checkboxes`,
|
|
89
93
|
});
|
|
90
94
|
}
|
|
91
95
|
}
|
|
@@ -99,16 +103,6 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
|
|
|
99
103
|
});
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
const drift = checkDrift(planningDir);
|
|
103
|
-
if (drift.drifted) {
|
|
104
|
-
warnings.push({
|
|
105
|
-
id: 'W12',
|
|
106
|
-
severity: 'WARN',
|
|
107
|
-
message: `Planning state drifted since last recorded session (${drift.details.join('; ')})`,
|
|
108
|
-
fix: 'Review the changed planning files. If the drift is intentional, rebaseline with `node .planning/bin/gsdd.mjs session-fingerprint write`, then rerun the blocked lifecycle preflight.',
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
106
|
return warnings;
|
|
113
107
|
}
|
|
114
108
|
|
|
@@ -156,6 +150,7 @@ function extractRepoLocalPaths(content) {
|
|
|
156
150
|
'CHANGELOG.md',
|
|
157
151
|
'SPEC.md',
|
|
158
152
|
'package.json',
|
|
153
|
+
'.work/',
|
|
159
154
|
'.planning/',
|
|
160
155
|
'.internal-research/',
|
|
161
156
|
'.agents/',
|