gsdd-cli 0.22.0 → 0.24.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.
@@ -100,6 +100,7 @@ export function createCmdInit(ctx) {
100
100
  promptApi,
101
101
  preselectedConfig: interactiveSession.config,
102
102
  });
103
+ ensureGitignoreEntry(ctx.cwd, '.planning/.local/', ' - ensured .planning/.local/ is gitignored');
103
104
 
104
105
  if (briefSource) {
105
106
  cpSync(briefSource, join(ctx.planningDir, 'PROJECT_BRIEF.md'));
@@ -273,7 +274,7 @@ async function ensureConfig({ cwd, planningDir, isAuto, promptApi, preselectedCo
273
274
  if (preselectedConfig) {
274
275
  writeFileSync(configFile, JSON.stringify(preselectedConfig, null, 2));
275
276
  console.log(' - saved .planning/config.json (guided wizard)\n');
276
- if (!preselectedConfig.commitDocs) ensureGitignoreEntry(cwd);
277
+ if (!preselectedConfig.commitDocs) ensureGitignoreEntry(cwd, '.planning/', ' - ensured .planning/ is gitignored');
277
278
  return;
278
279
  }
279
280
 
@@ -281,7 +282,7 @@ async function ensureConfig({ cwd, planningDir, isAuto, promptApi, preselectedCo
281
282
  const config = buildDefaultConfig({ autoAdvance: true });
282
283
  writeFileSync(configFile, JSON.stringify(config, null, 2));
283
284
  console.log(' - wrote .planning/config.json (auto defaults)\n');
284
- if (!config.commitDocs) ensureGitignoreEntry(cwd);
285
+ if (!config.commitDocs) ensureGitignoreEntry(cwd, '.planning/', ' - ensured .planning/ is gitignored');
285
286
  return;
286
287
  }
287
288
 
@@ -289,7 +290,7 @@ async function ensureConfig({ cwd, planningDir, isAuto, promptApi, preselectedCo
289
290
  const config = buildDefaultConfig({ autoAdvance: false });
290
291
  writeFileSync(configFile, JSON.stringify(config, null, 2));
291
292
  console.log(' - wrote .planning/config.json (non-interactive defaults)\n');
292
- if (!config.commitDocs) ensureGitignoreEntry(cwd);
293
+ if (!config.commitDocs) ensureGitignoreEntry(cwd, '.planning/', ' - ensured .planning/ is gitignored');
293
294
  return;
294
295
  }
295
296
 
@@ -303,18 +304,17 @@ async function ensureConfig({ cwd, planningDir, isAuto, promptApi, preselectedCo
303
304
 
304
305
  writeFileSync(configFile, JSON.stringify(selected, null, 2));
305
306
  console.log(' - saved .planning/config.json (guided wizard)\n');
306
- if (!selected.commitDocs) ensureGitignoreEntry(cwd);
307
+ if (!selected.commitDocs) ensureGitignoreEntry(cwd, '.planning/', ' - ensured .planning/ is gitignored');
307
308
  }
308
309
 
309
- function ensureGitignoreEntry(cwd) {
310
+ function ensureGitignoreEntry(cwd, entry, message) {
310
311
  const gitignorePath = join(cwd, '.gitignore');
311
- const entry = '.planning/';
312
312
  const hasGitignore = existsSync(gitignorePath);
313
313
  const current = hasGitignore ? readFileSync(gitignorePath, 'utf-8') : '';
314
314
  if (!current.split(/\r?\n/).includes(entry)) {
315
315
  const next = current.trimEnd() ? `${current.trimEnd()}\n${entry}\n` : `${entry}\n`;
316
316
  writeFileSync(gitignorePath, next);
317
- console.log(' - ensured .planning/ is gitignored');
317
+ console.log(message);
318
318
  }
319
319
  }
320
320
 
@@ -192,6 +192,8 @@ Commands:
192
192
  Validate UI proof metadata; use --claim for stronger proof uses
193
193
  ui-proof compare <planned-slots-json> [observed-bundle-json ...]
194
194
  Compare planned UI proof slots against observed bundles
195
+ control-map [--json] [--with-ignored] [--annotations <path>]
196
+ Report computed repo/worktree/planning state and local annotations
195
197
  help Show this summary
196
198
 
197
199
  Platforms (for --tools):
@@ -263,6 +265,7 @@ Advanced/internal helpers (kept available, but not the primary first-run user st
263
265
  session-fingerprint Rebaseline the local planning-state fingerprint after review
264
266
  phase-status Update ROADMAP.md phase status through the local helper surface
265
267
  ui-proof Validate UI proof metadata and compare planned slots to observed bundles
268
+ control-map Report computed repo/worktree/planning state and local annotations
266
269
  file-op Deterministic workspace-confined file copy/delete/text mutation
267
270
  `;
268
271
  }
package/bin/lib/phase.mjs CHANGED
@@ -4,10 +4,16 @@
4
4
  // evaluate once, so CWD must be computed inside function bodies.
5
5
 
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
7
- import { join, basename } from 'path';
7
+ import { dirname, join, relative } from 'path';
8
8
  import { output } from './cli-utils.mjs';
9
9
  import { writeFingerprint } from './session-fingerprint.mjs';
10
10
  import { resolveWorkspaceContext } from './workspace-root.mjs';
11
+ import {
12
+ compareUiProofSlots,
13
+ findUiProofBundleFiles,
14
+ parseUiProofSlotsContent,
15
+ readUiProofBundleFile,
16
+ } from './ui-proof.mjs';
11
17
 
12
18
  const PHASE_STATUS_MARKERS = {
13
19
  not_started: '[ ]',
@@ -169,6 +175,200 @@ function extractPlanFileArtifacts(planContent, workspaceRoot) {
169
175
  return artifacts;
170
176
  }
171
177
 
178
+ function isPlanArtifactSatisfied(artifact) {
179
+ if (artifact.operation === 'delete') return !artifact.exists;
180
+ return artifact.exists;
181
+ }
182
+
183
+ function planArtifactFixHint(artifact) {
184
+ if (artifact.operation === 'delete') {
185
+ return `Complete the planned DELETE for ${artifact.file}, or revise the plan if the file should remain.`;
186
+ }
187
+ return `Create or update ${artifact.file} so the planned ${artifact.operation.toUpperCase()} artifact exists, or revise the plan if it is no longer in scope.`;
188
+ }
189
+
190
+ function evaluatePlanArtifacts(artifacts) {
191
+ const unsatisfied = artifacts
192
+ .filter((artifact) => !isPlanArtifactSatisfied(artifact))
193
+ .map((artifact) => ({
194
+ ...artifact,
195
+ severity: 'blocker',
196
+ expected: artifact.operation === 'delete' ? 'absent' : 'present',
197
+ fix_hint: planArtifactFixHint(artifact),
198
+ }));
199
+ return {
200
+ satisfied: unsatisfied.length === 0,
201
+ unsatisfied,
202
+ };
203
+ }
204
+
205
+ function normalizeUiProofIssue(issue) {
206
+ return {
207
+ ...issue,
208
+ severity: issue.severity || 'blocker',
209
+ fix_hint: issue.fix_hint || issue.fix || 'Fix the UI proof issue before claiming verification is complete.',
210
+ };
211
+ }
212
+
213
+ function planDeclaresUiProofSlots(planContent) {
214
+ const match = String(planContent || '').match(/(^|\n)ui_proof_slots:[ \t]*([^\n]*)/);
215
+ if (!match) return false;
216
+ const inlineValue = match[2].replace(/\s+#.*$/, '').trim();
217
+ if (inlineValue) return !['[]', 'null', '~'].includes(inlineValue);
218
+ const after = String(planContent || '').slice(match.index + match[0].length).split(/\r?\n/);
219
+ for (const line of after) {
220
+ const trimmed = line.trim();
221
+ if (!trimmed || trimmed.startsWith('#')) continue;
222
+ if (trimmed === '---' || trimmed === '...') break;
223
+ if (/^\s+-\s+/.test(line)) return true;
224
+ if (/^\S[^:\n]*:\s*/.test(line) || /^\S/.test(line)) break;
225
+ }
226
+ return false;
227
+ }
228
+
229
+ function extractDeclaredUiProofSlotIds(planContent) {
230
+ const match = String(planContent || '').match(/(^|\n)ui_proof_slots:[ \t]*([^\n]*)/);
231
+ if (!match) return [];
232
+ const after = String(planContent || '').slice(match.index + match[0].length).split(/\r?\n/);
233
+ const slotIds = [];
234
+ for (const line of after) {
235
+ const trimmed = line.trim();
236
+ if (!trimmed || trimmed.startsWith('#')) continue;
237
+ if (trimmed === '---' || trimmed === '...') break;
238
+ if (/^\S[^:\n]*:\s*/.test(line) || /^\S/.test(line)) break;
239
+ const slotMatch = trimmed.match(/(?:^-\s*)?slot_id:\s*([^#\s]+)/);
240
+ if (slotMatch) slotIds.push(slotMatch[1].replace(/^['"]|['"]$/g, ''));
241
+ }
242
+ return slotIds;
243
+ }
244
+
245
+ function findUiProofSlotPlansAndFiles(planningDir, planDisplayPaths) {
246
+ const candidates = new Set();
247
+ const declaredPlans = [];
248
+ const declaredSlotIds = [];
249
+ const names = new Set([
250
+ 'ui-proof-slots.json',
251
+ 'ui-proof-slots.md',
252
+ 'UI-PROOF-SLOTS.json',
253
+ 'UI-PROOF-SLOTS.md',
254
+ 'planned-ui-proof.json',
255
+ 'planned-ui-proof.md',
256
+ ]);
257
+
258
+ for (const planDisplayPath of planDisplayPaths) {
259
+ const fullPlanPath = join(planningDir, 'phases', planDisplayPath);
260
+ if (!existsSync(fullPlanPath)) continue;
261
+ const planContent = readFileSync(fullPlanPath, 'utf-8');
262
+ if (!planDeclaresUiProofSlots(planContent)) continue;
263
+ const relPlanPath = relative(planningDir, fullPlanPath).replace(/\\/g, '/');
264
+ declaredPlans.push(relPlanPath);
265
+ for (const slotId of extractDeclaredUiProofSlotIds(planContent)) {
266
+ declaredSlotIds.push({ plan: relPlanPath, slot_id: slotId });
267
+ }
268
+ const planDir = dirname(fullPlanPath);
269
+ if (!existsSync(planDir)) continue;
270
+ for (const entry of readdirSync(planDir, { withFileTypes: true })) {
271
+ if (entry.isFile() && names.has(entry.name)) {
272
+ candidates.add(join(planDir, entry.name));
273
+ }
274
+ }
275
+ }
276
+ return { declaredPlans, declaredSlotIds, files: [...candidates].sort() };
277
+ }
278
+
279
+ function comparePhaseUiProof({ planningDir, workspaceRoot, planDisplayPaths }) {
280
+ const plannedDiscovery = findUiProofSlotPlansAndFiles(planningDir, planDisplayPaths);
281
+ const plannedFiles = plannedDiscovery.files;
282
+ const phaseDirs = new Set(planDisplayPaths.map((planDisplayPath) => dirname(join(planningDir, 'phases', planDisplayPath))));
283
+ const observedFiles = findUiProofBundleFiles(planningDir)
284
+ .filter((filePath) => phaseDirs.has(dirname(filePath)));
285
+
286
+ const plannedSlots = [];
287
+ const errors = [];
288
+ const planned = [];
289
+ const observed = [];
290
+
291
+ for (const filePath of plannedFiles) {
292
+ const rel = relative(workspaceRoot, filePath).replace(/\\/g, '/');
293
+ const parsed = parseUiProofSlotsContent(readFileSync(filePath, 'utf-8'), rel);
294
+ planned.push(rel);
295
+ plannedSlots.push(...parsed.slots);
296
+ errors.push(...parsed.errors.map(normalizeUiProofIssue));
297
+ }
298
+
299
+ if (plannedSlots.length > 0 && plannedDiscovery.declaredSlotIds.length > 0) {
300
+ const plannedSlotIds = new Set(plannedSlots.map((slot) => String(slot?.slot_id || '')));
301
+ for (const declaredSlot of plannedDiscovery.declaredSlotIds) {
302
+ if (plannedSlotIds.has(String(declaredSlot.slot_id))) continue;
303
+ errors.push(normalizeUiProofIssue({
304
+ code: 'planned_ui_proof_slots_drift',
305
+ path: `${declaredSlot.plan}.ui_proof_slots`,
306
+ message: `Plan declares UI proof slot ${declaredSlot.slot_id}, but no matching slot exists in the planned UI proof artifact.`,
307
+ fix: 'Update ui-proof-slots.json or ui-proof-slots.md beside the plan so it matches the plan-declared slot IDs, or update the plan declaration.',
308
+ }));
309
+ }
310
+ }
311
+
312
+ const observedBundles = [];
313
+ for (const filePath of observedFiles) {
314
+ const rel = relative(workspaceRoot, filePath).replace(/\\/g, '/');
315
+ const parsed = readUiProofBundleFile(filePath);
316
+ observed.push(rel);
317
+ if (parsed.errors.length > 0) {
318
+ errors.push(...parsed.errors.map((error) => normalizeUiProofIssue({ ...error, path: error.path || rel })));
319
+ continue;
320
+ }
321
+ observedBundles.push({
322
+ source: rel,
323
+ bundle: parsed.bundle,
324
+ options: {
325
+ requireLocalArtifactExists: true,
326
+ workspaceRoot,
327
+ bundleDir: dirname(filePath),
328
+ },
329
+ });
330
+ }
331
+
332
+ if (plannedFiles.length === 0 && plannedDiscovery.declaredPlans.length > 0) {
333
+ const missingError = {
334
+ code: 'missing_planned_ui_proof_slots_file',
335
+ severity: 'blocker',
336
+ path: plannedDiscovery.declaredPlans[0],
337
+ message: 'Plan declares ui_proof_slots but no ui-proof-slots artifact was found beside the plan.',
338
+ fix_hint: 'Create ui-proof-slots.json or ui-proof-slots.md beside the plan, or set ui_proof_slots: [] with a no_ui_proof_rationale if the phase is not UI-sensitive.',
339
+ };
340
+ return {
341
+ planned,
342
+ observed,
343
+ status: 'missing',
344
+ comparison: { status: 'missing', slots: [], errors: [missingError] },
345
+ errors: [missingError],
346
+ };
347
+ }
348
+
349
+ if (plannedFiles.length === 0) {
350
+ return {
351
+ planned,
352
+ observed,
353
+ status: 'not_applicable',
354
+ comparison: null,
355
+ errors,
356
+ };
357
+ }
358
+
359
+ const comparison = errors.length > 0
360
+ ? { status: 'partial', slots: [], errors: errors.map(normalizeUiProofIssue) }
361
+ : compareUiProofSlots(plannedSlots, observedBundles);
362
+
363
+ return {
364
+ planned,
365
+ observed,
366
+ status: comparison.status,
367
+ comparison,
368
+ errors: comparison.errors || errors,
369
+ };
370
+ }
371
+
172
372
  export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
173
373
  const marker = PHASE_STATUS_MARKERS[status];
174
374
  if (!marker) {
@@ -360,6 +560,26 @@ export function cmdVerify(...args) {
360
560
  ? extractPlanFileArtifacts(readFileSync(fullPath, 'utf-8'), workspaceRoot)
361
561
  : [];
362
562
  });
563
+ const artifactStatus = evaluatePlanArtifacts(artifacts);
564
+ const uiProof = comparePhaseUiProof({
565
+ planningDir,
566
+ workspaceRoot,
567
+ planDisplayPaths: matchingPlans,
568
+ });
569
+ const uiProofSatisfied = ['satisfied', 'not_applicable'].includes(uiProof.status);
570
+ const legacyVerified = matchingPlans.length > 0 && matchingSummaries.length > 0;
571
+ const uiProofGate = {
572
+ status: uiProof.status,
573
+ required: uiProof.status !== 'not_applicable',
574
+ satisfied: uiProofSatisfied,
575
+ blocks_verification: uiProof.status !== 'not_applicable' && !uiProofSatisfied,
576
+ required_block: uiProof.status !== 'not_applicable' && !uiProofSatisfied ? 'ui-proof-failed' : null,
577
+ };
578
+ const blockedOn = [
579
+ ...(artifactStatus.satisfied ? [] : ['artifacts']),
580
+ ...(uiProofGate.blocks_verification ? ['ui_proof'] : []),
581
+ ];
582
+ const closureVerified = legacyVerified && artifactStatus.satisfied && uiProofSatisfied;
363
583
 
364
584
  const result = {
365
585
  phase: normalizePhaseToken(phaseNum),
@@ -368,9 +588,17 @@ export function cmdVerify(...args) {
368
588
  summaries: matchingSummaries,
369
589
  artifacts,
370
590
  allExist: artifacts.every((artifact) => artifact.exists),
371
- verified: matchingPlans.length > 0 && matchingSummaries.length > 0,
591
+ artifact_status: artifactStatus,
592
+ uiProof,
593
+ verified: closureVerified,
594
+ legacy_verified: legacyVerified,
595
+ phase_artifacts_present: legacyVerified,
596
+ ui_proof: uiProofGate,
597
+ blocked_on: blockedOn,
598
+ blocks_verification: blockedOn.length > 0,
372
599
  };
373
600
  output(result);
601
+ if (!closureVerified && legacyVerified) process.exitCode = 1;
374
602
  }
375
603
 
376
604
  export function cmdScaffold(...args) {
@@ -7,6 +7,7 @@ const __dirname = dirname(__filename);
7
7
  const DISTILLED_DIR = join(__dirname, '..', '..', 'distilled');
8
8
  const HELPER_LIB_FILES = Object.freeze([
9
9
  'cli-utils.mjs',
10
+ 'control-map.mjs',
10
11
  'evidence-contract.mjs',
11
12
  'file-ops.mjs',
12
13
  'lifecycle-preflight.mjs',
@@ -49,6 +50,7 @@ import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
49
50
  import { cmdPhaseStatus } from './lib/phase.mjs';
50
51
  import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
51
52
  import { cmdUiProof } from './lib/ui-proof.mjs';
53
+ import { cmdControlMap } from './lib/control-map.mjs';
52
54
  import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs';
53
55
 
54
56
  const COMMANDS = {
@@ -57,6 +59,7 @@ const COMMANDS = {
57
59
  'phase-status': cmdPhaseStatus,
58
60
  'session-fingerprint': cmdSessionFingerprint,
59
61
  'ui-proof': cmdUiProof,
62
+ 'control-map': cmdControlMap,
60
63
  };
61
64
 
62
65
  function printHelp() {
@@ -78,6 +81,8 @@ function printHelp() {
78
81
  ' Validate UI proof metadata; use --claim for stronger proof uses',
79
82
  ' ui-proof compare <planned-slots-json> [observed-bundle-json ...]',
80
83
  ' Compare planned UI proof slots against observed bundles',
84
+ ' control-map [--json] [--with-ignored] [--annotations <path>]',
85
+ ' Report computed repo/worktree/planning state and local annotations',
81
86
  '',
82
87
  'Advanced option:',
83
88
  ' --workspace-root <path> Override workspace root discovery before or after the subcommand',
@@ -446,6 +446,41 @@ function normalizeObservedBundle(entry) {
446
446
  };
447
447
  }
448
448
 
449
+ function comparisonFixHint(code) {
450
+ const hints = {
451
+ invalid_observed_bundle: 'Fix the observed proof bundle metadata, then rerun ui-proof compare.',
452
+ unsatisfied_observed_claim_status: 'Record a passed observed claim only after the changed UI state has been exercised and evidenced.',
453
+ unsatisfied_observed_comparison_status: 'Set comparison_status_by_slot to satisfied only for slots backed by matching observations and artifacts.',
454
+ missing_required_evidence_kind: 'Add observed evidence for every evidence kind required by the planned slot, or narrow the planned slot before verification.',
455
+ human_evidence_cannot_bypass_required_non_human_evidence: 'Add the missing non-human evidence; human approval may narrow or waive but cannot replace it.',
456
+ route_state_mismatch: 'Capture proof for the exact planned route/state, or update the plan before execution.',
457
+ environment_mismatch: 'Capture proof in the planned environment, or record a narrowed claim limit and rerun comparison.',
458
+ viewport_mismatch: 'Capture proof for the planned viewport, or narrow the viewport claim explicitly.',
459
+ requirement_mismatch: 'Declare the planned requirement id in the observed proof bundle scope.',
460
+ claim_mismatch: 'Keep the planned and observed claims identical so proof maps to the exact UI assertion.',
461
+ observation_claim_mismatch: 'Add a passed observation that supports the exact planned claim.',
462
+ observation_route_state_mismatch: 'Attach observations to the exact planned route/state.',
463
+ missing_supporting_observation_evidence_kind: 'Add passed supporting observations for each required evidence kind.',
464
+ unsatisfied_proof_step: 'Rerun or replace failing proof steps before claiming the slot is satisfied.',
465
+ missing_manual_acceptance_evidence: 'Record human evidence when the planned slot requires manual acceptance.',
466
+ missing_manual_acceptance_observation: 'Add a passed human observation for manual acceptance.',
467
+ unsatisfied_observation_result: 'Resolve failed observations or classify the slot as partial, waived, or deferred.',
468
+ missing_minimum_observation: 'Add observations covering every planned minimum observation.',
469
+ missing_claim_limit: 'Preserve the planned claim limit in the observed proof bundle.',
470
+ missing_expected_artifact_type: 'Attach the planned artifact type, such as screenshot, report, trace, or DOM snapshot.',
471
+ missing_observed_bundle: 'Create an observed UI proof bundle for the planned slot, or explicitly waive/defer the slot with claim narrowing.',
472
+ };
473
+ return hints[code] || 'Fix the proof issue, rerun the comparison, and keep the slot partial until evidence matches the plan.';
474
+ }
475
+
476
+ function decorateComparisonIssue(issue) {
477
+ return {
478
+ severity: issue.severity || 'blocker',
479
+ fix_hint: issue.fix_hint || issue.fix || comparisonFixHint(issue.code),
480
+ ...issue,
481
+ };
482
+ }
483
+
449
484
  function compareSlotToBundle(slot, slotIdValue, observed) {
450
485
  const issues = [];
451
486
  const bundle = observed.bundle;
@@ -648,7 +683,7 @@ function compareSlotToBundle(slot, slotIdValue, observed) {
648
683
  }
649
684
 
650
685
  const status = issues.length === 0 ? 'satisfied' : (bundleStatus === 'missing' ? 'missing' : 'partial');
651
- return { status, issues, source: observed.source };
686
+ return { status, issues: issues.map(decorateComparisonIssue), source: observed.source };
652
687
  }
653
688
 
654
689
  export function compareUiProofSlots(plannedSlots, observedBundles) {
@@ -656,7 +691,7 @@ export function compareUiProofSlots(plannedSlots, observedBundles) {
656
691
  const slotValidation = validateUiProofSlots(slots);
657
692
  const bundles = normalizeArray(observedBundles).map(normalizeObservedBundle);
658
693
  const results = [];
659
- const errors = [...slotValidation.errors];
694
+ const errors = slotValidation.errors.map(decorateComparisonIssue);
660
695
 
661
696
  for (const observed of bundles) {
662
697
  if (!observed.validation.valid) {
@@ -680,7 +715,7 @@ export function compareUiProofSlots(plannedSlots, observedBundles) {
680
715
  code: 'missing_observed_bundle',
681
716
  path: 'scope.slot_ids',
682
717
  message: `No observed UI proof bundle declares planned slot ${slotIdValue}.`,
683
- }],
718
+ }].map(decorateComparisonIssue),
684
719
  });
685
720
  continue;
686
721
  }
@@ -706,7 +741,7 @@ export function compareUiProofSlots(plannedSlots, observedBundles) {
706
741
  ? 'missing'
707
742
  : 'partial';
708
743
 
709
- return { status, slots: results, errors };
744
+ return { status, slots: results, errors: errors.map(decorateComparisonIssue) };
710
745
  }
711
746
 
712
747
  export function validateUiProofBundle(bundle, options = {}) {
@@ -73,6 +73,7 @@
73
73
  60. [Release Closeout Contract](#d60---release-closeout-contract)
74
74
  61. [Deliberate Subagent Contract](#d61---deliberate-subagent-contract)
75
75
  62. [Repo-Native UI Proof Contract](#d62---repo-native-ui-proof-contract)
76
+ 63. [Computed-First Control Map](#d63---computed-first-control-map)
76
77
 
77
78
  ---
78
79
 
@@ -2872,6 +2873,44 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru
2872
2873
  - Generated runtime surfaces and local templates must stay freshness-checkable through `gsdd update --templates` and health diagnostics.
2873
2874
  - Future provider/tooling work must not make `agent-browser` a required validator field without a separate product decision; the current contract makes it the default workflow path, not a schema lock.
2874
2875
 
2876
+ ## D63 - Computed-First Control Map
2877
+
2878
+ **Decision (2026-05-08):** Long-running multi-agent and multi-worktree control uses a computed-first `gsdd control-map` helper rather than a new lifecycle workflow or a vendor session parser. The helper computes repo/worktree/planning truth live and overlays optional local annotations only for intent that git cannot know.
2879
+
2880
+ **Context:**
2881
+ - Gap I52 showed that ordinary `git status` can be clean while sibling worktrees, detached runtime worktrees, ignored/generated surfaces, snapshots, dirty local WIP, and cleanup obligations remain unexplained.
2882
+ - Gap I54 showed that repeated subagent swarms can become a substitute for shared state when there is no one-screen control map for active branches, ownership, proof state, and cleanup debt.
2883
+ - Current runtime research shows the only portable cross-vendor coordination layer is repo artifacts plus generated workflow entrypoints. Claude, OpenCode, Codex, Cursor, Copilot, and Gemini do not expose one uniform authoritative session/worktree store.
2884
+ - Current harness guidance favors structured handoff artifacts, worktree isolation, evaluator loops, approval gates around side effects, and browser/runtime evidence. Those ideas fit Workspine only if repo truth remains primary and vendor adapters stay thin.
2885
+
2886
+ **Decision:**
2887
+ - Add `gsdd control-map [--json] [--with-ignored] [--annotations <path>]` to the main CLI and generated `.planning/bin/gsdd.mjs` helper runtime.
2888
+ - Compute authority from live git/worktree state first: canonical checkout, branch, HEAD, upstream divergence when comparable, tracked/untracked dirty buckets, optional ignored-path scans through `--with-ignored`, sibling git worktrees, detached/bare state, invalid git access, planning drift, checkpoint existence, lifecycle state, and repo-local runtime worktree directories.
2889
+ - Read optional annotations from `.planning/.local/control-map.annotations.json`. Annotations may record `runtime_owner`, intended scope, write set, cleanup state, proof state, next step, branch, and last known head.
2890
+ - Treat annotations as stale-checkable intent only. They never outrank repo truth, planning artifacts, or checkpoint reconciliation.
2891
+ - Keep transcript/session stores out of the helper. Vendor session evidence may support postmortems, but it is not live product truth.
2892
+ - Wire the control map into portable workflow behavior by having `progress`, `resume`, `pause`, `quick`, `plan`, and `execute` consult it when available. This is guidance plus deterministic helper output, not a new workflow lane.
2893
+
2894
+ **Leverage:**
2895
+ - Lost: a pure zero-file model cannot preserve non-computable intent such as owner/runtime, intended scope, and cleanup obligation.
2896
+ - Kept: Workspine remains a lightweight repo-native spine; no new lifecycle workflow, no dashboard/control plane, no vendor session authority, and no change to the five evidence kinds.
2897
+ - Gained: agents can explain "clean" precisely across tracked, untracked, sibling, detached, stale, and annotated state by default, and across ignored/generated local surfaces when the caller requests the explicit `--with-ignored` scan before planning, execution, resume, cleanup, or milestone continuation.
2898
+
2899
+ **Evidence:**
2900
+ - `bin/lib/control-map.mjs`, `bin/gsdd.mjs`, `bin/lib/rendering.mjs`
2901
+ - `distilled/workflows/progress.md`, `resume.md`, `pause.md`, `quick.md`, `plan.md`, `execute.md`
2902
+ - `tests/gsdd.control-map.test.cjs`
2903
+ - `.internal-research/gaps.md` Gap I52 and Gap I54
2904
+ - `.internal-research/lessons-learned.md` entries on multi-worktree registry, clean-vs-editor-visible noise, checkpoint/worktree truth split, and subagent stop conditions
2905
+ - GSD comparison: upstream GSD preserves lifecycle rigor but does not define a vendor-agnostic computed worktree/control-map helper.
2906
+ - OpenSpec comparison: OpenSpec optimizes change-level speed and archive flow, but does not own long-running multi-worktree local-state reconciliation as a portable harness surface.
2907
+ - Harness sources: `https://www.anthropic.com/engineering/harness-design-long-running-apps`, `https://code.claude.com/docs/en/worktrees`, `https://developers.openai.com/codex/cloud`, `https://developers.openai.com/api/docs/guides/agents/orchestration`, `https://developers.openai.com/api/docs/guides/agents/guardrails-approvals`, `https://developers.openai.com/api/docs/guides/agent-evals`, `https://docs.github.com/en/copilot/concepts/agents/cloud-agent/about-cloud-agent`, `https://agent-browser.dev/sessions`, and `https://developer.chrome.com/docs/devtools/agents`.
2908
+
2909
+ **Consequences:**
2910
+ - Future cleanup, resume, and parallel-worktree work should start from `gsdd control-map --json` rather than repeated ad hoc repo audits; use `--with-ignored` before making a clean-workspace claim that includes ignored or generated surfaces.
2911
+ - A future mutation command may update annotations, but the current helper intentionally stays computed/read-first and safe to call from status surfaces.
2912
+ - Future health or preflight hardening can consume the same helper output for stricter blocking, but must avoid turning local annotations into product truth.
2913
+
2875
2914
  ---
2876
2915
 
2877
2916
  ## Maintenance
@@ -505,6 +505,16 @@
505
505
  - Long-term pitfalls carried forward: do not accept screenshot-free "looks good" claims, weak planned slots, unverified artifact paths, stale interactive refs after page mutation, partial/failed proof without failure classification, raw artifact publication without privacy metadata, browser contention in parallel checks, or semantic/selector-poor UI that forces fragile coordinate inspection.
506
506
  - Supporting spec/runtime docs: https://openspec.dev/, https://www.lean-spec.dev/docs/guide/first-principles, https://help.openai.com/en/articles/11369540-codex-in-chatgpt, https://docs.claude.com/en/docs/agents-and-tools/agent-skills, https://docs.github.com/en/copilot/concepts/prompting/response-customization
507
507
 
508
+ ## D63 — Computed-First Control Map
509
+ - `bin/lib/control-map.mjs`, `bin/gsdd.mjs`, `bin/lib/rendering.mjs`
510
+ - `distilled/workflows/progress.md`, `resume.md`, `pause.md`, `quick.md`, `plan.md`, `execute.md`
511
+ - `tests/gsdd.control-map.test.cjs`
512
+ - `.internal-research/gaps.md` Gap I52 and Gap I54
513
+ - `.internal-research/lessons-learned.md` multi-worktree registry, clean-vs-editor-visible noise, checkpoint/worktree truth split, and subagent stop-condition lessons
514
+ - GSD comparison: upstream GSD lifecycle rigor does not include a vendor-agnostic computed worktree/control-map helper
515
+ - OpenSpec comparison: OpenSpec change archive flow does not own long-running multi-worktree local-state reconciliation as a portable harness surface
516
+ - Harness sources: https://www.anthropic.com/engineering/harness-design-long-running-apps, https://code.claude.com/docs/en/worktrees, https://developers.openai.com/codex/cloud, https://developers.openai.com/api/docs/guides/agents/orchestration, https://developers.openai.com/api/docs/guides/agents/guardrails-approvals, https://developers.openai.com/api/docs/guides/agent-evals, https://docs.github.com/en/copilot/concepts/agents/cloud-agent/about-cloud-agent, https://agent-browser.dev/sessions, https://developer.chrome.com/docs/devtools/agents
517
+
508
518
  ---
509
519
 
510
520
  ## Maintenance
@@ -71,6 +71,14 @@ npx -y gsdd-cli init -> bootstrap (create .planning/, copy templates, gene
71
71
  /gsdd-progress -> show status, route to next action
72
72
  ```
73
73
 
74
+ The main operator spine is four workflow moves after bootstrap: `new-project -> plan -> execute -> verify`. The other public workflow surfaces are support lanes for milestone closeout, quick work, progress, pause/resume, and brownfield orientation.
75
+
76
+ Helper command for long-running sessions:
77
+
78
+ ```
79
+ npx -y gsdd-cli control-map [--json] [--with-ignored] -> computed repo/worktree/planning state plus local annotations
80
+ ```
81
+
74
82
  ## Brownfield Entry Contract
75
83
 
76
84
  Use the same three-way routing everywhere:
@@ -101,6 +109,7 @@ Use the same three-way routing everywhere:
101
109
  Architecture notes:
102
110
  - `bin/gsdd.mjs` remains the thin generator entrypoint, while vendor-specific rendering lives in adapter modules.
103
111
  - Codex CLI uses the always-generated `.agents/skills/gsdd-*` surface as its entry path, relies on `.planning/bin/gsdd.mjs` for deterministic helper calls, and can add a native `.codex/agents/gsdd-plan-checker.toml` checker agent.
112
+ - `control-map` is a helper command, not a lifecycle workflow: it computes repo/worktree/planning truth first and treats `.planning/.local/` annotations as local intent only.
104
113
  - Codex VS Code/app are separate surfaces from Codex CLI; do not claim the CLI proof for them unless they expose compatible skill discovery. Fallback is opening or pasting the generated `SKILL.md`.
105
114
  - `npx -y gsdd-cli health` now compares any installed generated runtime surfaces against current render output and routes repairs back through `npx -y gsdd-cli update`.
106
115
  - Portable lifecycle contracts now align to the roadmap template status grammar: `[ ]`, `[-]`, `[x]`.
@@ -1,8 +1,7 @@
1
1
  <role>
2
2
  You are the EXECUTOR. Your job is to implement the tasks from a phase plan with precision and discipline.
3
3
 
4
- You follow the plan. You verify before reporting completion. You document deviations.
5
- You DO NOT freelance. You DO NOT add features outside the plan.
4
+ You follow the plan, verify before reporting completion, document deviations, and DO NOT freelance or add features outside the plan.
6
5
  </role>
7
6
 
8
7
  <load_context>
@@ -36,7 +35,8 @@ Treat the preflight as an authorization seam over shared repo truth only:
36
35
  - it does not mutate `.planning/ROADMAP.md` by itself
37
36
  - owned writes remain execution artifacts, and ROADMAP mutation stays explicit in `<state_updates>` via `node .planning/bin/gsdd.mjs phase-status`
38
37
  </lifecycle_preflight>
39
-
38
+ <control_map_check>Before code mutation, run `node .planning/bin/gsdd.mjs control-map --json` when available. Confirm the intended execution surface, dirty buckets, sibling/detached worktrees, and overlapping write-set risk. If it reports stale annotations, dubious git access, dirty out-of-plan canonical files, or unannotated dirty sibling worktrees, stop or ask for explicit acknowledgement before broad writes. Local annotations are intent hints only; computed repo/worktree truth stays primary.
39
+ </control_map_check>
40
40
  <runtime_contract>
41
41
  Execution uses the same `Runtime` and `Assurance` types as planning and verification.
42
42
  Infer runtime from the launching surface when obvious: `.claude/` -> `claude-code`, `.codex/` or Codex portable skill -> `codex-cli`, `.opencode/` -> `opencode`, otherwise `other`.
@@ -39,6 +39,8 @@ Store the detected work type as `$WORK_TYPE` (one of: `phase`, `quick`, `generic
39
39
  <gather_state>
40
40
  Build a draft checkpoint from artifact truth before asking the user to restate work. The user should correct the draft, not rewrite obvious repo state from scratch.
41
41
 
42
+ When available, run `node .planning/bin/gsdd.mjs control-map --json` and use it as the draft's repo/worktree snapshot: canonical branch/HEAD, dirty tracked/untracked/ignored buckets, sibling/detached worktrees, stale annotations, planning drift, and recommended interventions. Include only a compact summary or pointer in `.planning/.continue-here.md`; the checkpoint records resumability context, not a replacement for future computed repo truth.
43
+
42
44
  Ask the user conversationally to fill in the gaps the artifacts cannot answer:
43
45
 
44
46
  1. **What was completed** this session
@@ -32,12 +32,12 @@ If the preflight result is `blocked`, STOP and report the blocker instead of inf
32
32
 
33
33
  <integration_surface_check>
34
34
  Before planning roadmap work, inspect the live integration surface separately from checkpoint or planning artifacts:
35
- - current branch name
36
- - divergence from `main` when available
37
- - staged, unstaged, and untracked local truth
38
- - whether the current branch appears stale/spent or mixed-scope
35
+ - Run `node .planning/bin/gsdd.mjs control-map --json` when available.
36
+ - Use its computed branch/HEAD, divergence, tracked/untracked/ignored buckets, sibling/detached worktrees, local annotations, and interventions.
37
+ - If the helper is unavailable, fall back to direct git/worktree inspection.
39
38
 
40
- If the planning truth says "next phase is X" but the git/worktree truth says the current branch is a stale or mixed execution surface, warn explicitly and treat the dirty branch as evidence only. Do not silently assume the checked-out branch is the right planning surface just because it exists.
39
+ If the planning truth says "next phase is X" but the git/worktree truth says the current branch is a stale/spent or mixed-scope execution surface, warn explicitly and treat the dirty branch as evidence only. Do not silently assume the checked-out branch is the right planning surface just because it exists.
40
+ Local annotations explain operator intent but do not outrank repo truth, planning artifacts, or checkpoint reconciliation.
41
41
  </integration_surface_check>
42
42
 
43
43
  <runtime_contract>
@@ -6,6 +6,10 @@ Core mindset: derive state from primary artifacts. ROADMAP.md checkboxes, phase
6
6
  Scope boundary: you are NOT resume.md. You do not wait for user input, clean up checkpoints, present interactive menus, or trigger any action. You report and suggest only.
7
7
  </role>
8
8
 
9
+ <control_map>
10
+ At the start of status reporting, run `node .planning/bin/gsdd.mjs control-map --json` when the local helper exists. Summarize its computed repo/worktree/planning state in the status block: canonical branch/HEAD, tracked/untracked dirty buckets, whether ignored paths were scanned, sibling or detached worktrees, stale local annotations, planning drift, and recommended interventions. Use `--with-ignored` before making a clean-workspace claim that includes ignored or generated surfaces. Treat the command output as read-only computed evidence. Local annotations under `.planning/.local/` explain intent but never outrank repo truth, planning artifacts, or checkpoint reconciliation.
11
+ </control_map>
12
+
9
13
  <prerequisites>
10
14
  `.planning/` must exist (from `npx -y gsdd-cli init`, or `gsdd init` when globally installed).
11
15
 
@@ -45,7 +45,7 @@ Store the response as `$DESCRIPTION`. If empty, re-prompt.
45
45
  6. If `.planning/codebase/` exists, read whichever of `.planning/codebase/ARCHITECTURE.md`, `.planning/codebase/STACK.md`, `.planning/codebase/CONVENTIONS.md`, and `.planning/codebase/CONCERNS.md` are present. Summarize key findings from available docs in <=500 words as `$CODEBASE_CONTEXT`, emphasizing: safest surfaces to touch, risky zones to avoid, must-know conventions/traps, and what must be re-verified after change. Note any missing docs in the summary.
46
46
  7. If `.planning/codebase/` does not exist, build a just-enough inline brownfield baseline instead of stopping. Read the repo root guidance that is cheap and stable (`README.md`, root manifest such as `package.json` / `pyproject.toml` / `Cargo.toml` when present, top-level app entrypoints, and any obviously relevant config or module files surfaced by `$DESCRIPTION`). Summarize the findings in <=500 words as `$CODEBASE_CONTEXT`, explicitly labeling it as a provisional baseline and calling out unknowns. Emphasize: likely implementation surface, likely dependency boundaries, conventions already visible, risky areas to avoid touching blindly, and what must be re-verified after the change. If the repo is still too unclear after this pass, keep that uncertainty explicit so Step 3.6 can recommend `/gsdd-map-codebase`.
47
47
  8. **Session-boundary fallback:** If `.planning/.continue-here.bak` exists, read its `<judgment>` section. Use `<active_constraints>` and `<anti_regression>` rules as task-scoping context (do not violate active constraints; do not regress on listed invariants). After reading, run `node .planning/bin/gsdd.mjs file-op delete .planning/.continue-here.bak --missing ok` (auto-clean).
48
- 9. Inspect the live branch/worktree surface separately from checkpoint or planning artifacts. If the current branch appears stale/spent, PR-less with overlapping write scope, or otherwise like the wrong execution surface, warn before proceeding. This is advisory for quick tasks unless the mismatch makes the task description materially misleading.
48
+ 9. Inspect the live branch/worktree surface separately from checkpoint or planning artifacts. Run `node .planning/bin/gsdd.mjs control-map --json` when available and use its computed repo/worktree/planning truth to identify stale/spent branches, dirty tracked/untracked/ignored buckets, sibling or detached worktrees, local annotations, and cleanup obligations. This is advisory for quick tasks unless the mismatch makes the task description materially misleading; local annotations are intent hints, not product truth.
49
49
 
50
50
  If `.planning/quick/` does not exist, create it along with an empty `LOG.md`:
51
51
 
@@ -35,6 +35,10 @@ Treat the preflight as an authorization seam over shared repo truth only:
35
35
 
36
36
  <process>
37
37
 
38
+ <control_map_reconciliation>
39
+ Before routing from a checkpoint, run `node .planning/bin/gsdd.mjs control-map --json` when available. Reconcile the checkpoint narrative against computed repo/worktree/planning truth: canonical branch/HEAD, tracked/untracked/ignored dirty buckets, sibling or detached worktrees, local annotations, active brownfield anchors, and planning drift. If the checkpoint understates dirty work, points at a stale branch/worktree, or conflicts with the active planning/brownfield surface, stop and present the mismatch before recommending execution. Local annotations are useful intent hints, not authority.
40
+ </control_map_reconciliation>
41
+
38
42
  <detect_state>
39
43
  Check for project artifacts in order:
40
44