gsdd-cli 0.21.0 → 0.23.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
  }
@@ -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',
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
- import { isAbsolute, join, relative, resolve } from 'path';
2
+ import { dirname, isAbsolute, join, relative, resolve } from 'path';
3
3
  import { output } from './cli-utils.mjs';
4
4
  import { resolveWorkspaceContext } from './workspace-root.mjs';
5
5
 
@@ -10,6 +10,8 @@ const ARTIFACT_VISIBILITIES = Object.freeze(['local_only', 'repo_tracked', 'publ
10
10
  const RAW_ARTIFACT_TYPES = Object.freeze(['screenshot', 'trace', 'video', 'dom_snapshot', 'dom-snapshot', 'dom', 'report']);
11
11
  const PUBLIC_CLAIM_USES = Object.freeze(['public', 'publication', 'tracked', 'delivery', 'release']);
12
12
  const CLAIM_USES = Object.freeze([...PUBLIC_CLAIM_USES, 'local', 'local_only']);
13
+ const FAILURE_CLASSIFICATIONS = Object.freeze(['product_bug', 'missing_infra', 'flaky_harness', 'ambiguous_spec']);
14
+ const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9_.:-]*$/;
13
15
  const REQUIRED_BUNDLE_FIELDS = Object.freeze([
14
16
  'proof_bundle_version',
15
17
  'scope',
@@ -25,6 +27,19 @@ const REQUIRED_BUNDLE_FIELDS = Object.freeze([
25
27
  'claim_limits',
26
28
  ]);
27
29
  const REQUIRED_SCOPE_FIELDS = Object.freeze(['work_item', 'claim', 'requirement_ids', 'slot_ids']);
30
+ const REQUIRED_SLOT_FIELDS = Object.freeze([
31
+ 'slot_id',
32
+ 'claim',
33
+ 'route_state',
34
+ 'required_evidence_kinds',
35
+ 'minimum_observations',
36
+ 'environment',
37
+ 'viewport',
38
+ 'expected_artifact_types',
39
+ 'validation_command',
40
+ 'manual_acceptance_required',
41
+ 'claim_limit',
42
+ ]);
28
43
  const REQUIRED_ARTIFACT_FIELDS = Object.freeze(['visibility', 'retention', 'sensitivity', 'safe_to_publish']);
29
44
  const REQUIRED_OBSERVATION_FIELDS = Object.freeze(['observation', 'claim', 'route_state', 'evidence_kind', 'artifact_refs', 'privacy', 'result', 'claim_limit']);
30
45
  const REQUIRED_PRIVACY_FIELDS = Object.freeze(['data_classification', 'raw_artifacts_safe_to_publish', 'retention']);
@@ -72,12 +87,12 @@ function normalizeArray(value) {
72
87
 
73
88
  function artifactType(artifact) {
74
89
  const explicit = typeof artifact.type === 'string' ? artifact.type.toLowerCase() : '';
75
- const artifactPath = typeof artifact.path === 'string' ? artifact.path.toLowerCase() : '';
76
- if (/screenshot|\.png$|\.jpe?g$|\.webp$/.test(artifactPath)) return 'screenshot';
77
- if (/trace|\.zip$/.test(artifactPath)) return 'trace';
78
- if (/video|\.mp4$|\.webm$|\.mov$/.test(artifactPath)) return 'video';
79
- if (/dom|\.html?$/.test(artifactPath)) return 'dom_snapshot';
80
- if (/report/.test(artifactPath)) return 'report';
90
+ const artifactRef = artifactReference(artifact)?.toLowerCase() || '';
91
+ if (/screenshot|\.png$|\.jpe?g$|\.webp$/.test(artifactRef)) return 'screenshot';
92
+ if (/trace|\.zip$/.test(artifactRef)) return 'trace';
93
+ if (/video|\.mp4$|\.webm$|\.mov$/.test(artifactRef)) return 'video';
94
+ if (/dom|\.html?$/.test(artifactRef)) return 'dom_snapshot';
95
+ if (/report/.test(artifactRef)) return 'report';
81
96
  return explicit;
82
97
  }
83
98
 
@@ -172,6 +187,19 @@ function validateEvidenceKinds(bundle, errors) {
172
187
  }
173
188
  }
174
189
 
190
+ function validateToolsUsed(bundle, errors) {
191
+ const tools = normalizeArray(bundle?.evidence_inputs?.tools_used);
192
+ if (tools.length === 0) {
193
+ addError(errors, 'missing_tools_used', 'evidence_inputs.tools_used', 'Missing UI proof tool provenance.', 'Record concise tool IDs such as browser, playwright, manual, or project-specific command IDs.');
194
+ return;
195
+ }
196
+ for (const [index, tool] of tools.entries()) {
197
+ if (!TOOL_ID_PATTERN.test(tool)) {
198
+ addError(errors, 'invalid_tool_id', `evidence_inputs.tools_used[${index}]`, `Invalid UI proof tool identifier: ${tool}`, 'Use a concise lowercase identifier without spaces, for example browser, playwright, manual, or gsdd-ui-proof-validate.');
199
+ }
200
+ }
201
+ }
202
+
175
203
  function validateResult(bundle, errors) {
176
204
  if (!isPlainObject(bundle?.result)) return;
177
205
  if (!hasValue(bundle.result.claim_status)) {
@@ -181,6 +209,28 @@ function validateResult(bundle, errors) {
181
209
  }
182
210
  }
183
211
 
212
+ function validateFailureClassification(bundle, errors) {
213
+ const statuses = [
214
+ bundle?.result?.claim_status,
215
+ ...Object.values(isPlainObject(bundle?.result?.comparison_status_by_slot) ? bundle.result.comparison_status_by_slot : {}),
216
+ ...normalizeArray(bundle?.observations).map((observation) => isPlainObject(observation) ? observation.result : null),
217
+ ...normalizeArray(bundle?.commands_or_manual_steps).map((step) => isPlainObject(step) ? step.result : null),
218
+ ].filter(Boolean);
219
+ const failedOrPartial = statuses.some((status) => status === 'failed' || status === 'partial');
220
+ const classifications = normalizeArray(bundle?.result?.failure_classification || bundle?.result?.failure_classifications);
221
+
222
+ if (failedOrPartial && classifications.length === 0) {
223
+ addError(errors, 'missing_failure_classification', 'result.failure_classification', 'Failed or partial UI proof must classify why it failed.', `Use one of: ${FAILURE_CLASSIFICATIONS.join(', ')}.`);
224
+ return;
225
+ }
226
+
227
+ for (const [index, classification] of classifications.entries()) {
228
+ if (!FAILURE_CLASSIFICATIONS.includes(classification)) {
229
+ addError(errors, 'invalid_failure_classification', `result.failure_classification[${index}]`, `Invalid UI proof failure classification: ${classification}`, `Use only: ${FAILURE_CLASSIFICATIONS.join(', ')}.`);
230
+ }
231
+ }
232
+ }
233
+
184
234
  function validateComparisonStatuses(bundle, errors) {
185
235
  const statuses = bundle?.result?.comparison_status_by_slot;
186
236
  if (!isPlainObject(statuses)) {
@@ -202,6 +252,10 @@ function validateComparisonStatuses(bundle, errors) {
202
252
  addError(errors, 'invalid_comparison_status', `result.comparison_status_by_slot.${slot}`, `Invalid UI proof comparison status: ${status}`, `Use only: ${COMPARISON_STATUSES.join(', ')}.`);
203
253
  }
204
254
  }
255
+ const unsatisfiedStatuses = Object.values(statuses).filter((status) => !['satisfied', 'not_applicable'].includes(status));
256
+ if (bundle?.result?.claim_status === 'passed' && unsatisfiedStatuses.length > 0) {
257
+ addError(errors, 'inconsistent_claim_status', 'result.claim_status', 'UI proof claim_status cannot be passed when comparison statuses are unsatisfied.', 'Use partial, failed, waived, deferred, or not_applicable when any slot comparison is not satisfied.');
258
+ }
205
259
  }
206
260
 
207
261
  function validateClaimLimits(bundle, errors) {
@@ -234,7 +288,7 @@ function isSanitizedSensitivity(value) {
234
288
  return typeof value === 'string' && /(^|[_\s-])(sanitized|public_safe|public-safe)($|[_\s-])/.test(value.toLowerCase());
235
289
  }
236
290
 
237
- function validateArtifacts(bundle, errors, publicClaim) {
291
+ function validateArtifacts(bundle, errors, publicClaim, options = {}) {
238
292
  const artifacts = normalizeArray(bundle?.artifacts);
239
293
  if (artifacts.length === 0) {
240
294
  addError(errors, 'missing_artifacts', 'artifacts', 'Missing UI proof artifacts list.', 'Record artifact metadata for each referenced proof artifact.');
@@ -254,6 +308,12 @@ function validateArtifacts(bundle, errors, publicClaim) {
254
308
  } else {
255
309
  validateArtifactReferenceSafety(ref, artifactPath, errors);
256
310
  artifactRefs.add(ref);
311
+ if (options.requireLocalArtifactExists && !/^https?:\/\//i.test(ref) && hasValue(options.workspaceRoot)) {
312
+ const artifactFile = resolve(options.workspaceRoot, ref);
313
+ if (!existsSync(artifactFile) || statSync(artifactFile).isDirectory()) {
314
+ addError(errors, 'missing_local_artifact', artifactPath, `UI proof artifact file does not exist: ${ref}`, 'Create the referenced artifact, correct the path, or narrow the proof claim.');
315
+ }
316
+ }
257
317
  }
258
318
  for (const field of REQUIRED_ARTIFACT_FIELDS) {
259
319
  requireField(artifact, field, artifactPath, errors);
@@ -277,6 +337,40 @@ function validateArtifacts(bundle, errors, publicClaim) {
277
337
  return artifactRefs;
278
338
  }
279
339
 
340
+ export function validateUiProofSlots(slots) {
341
+ const errors = [];
342
+ const normalizedSlots = normalizeArray(slots);
343
+ if (normalizedSlots.length === 0) {
344
+ addError(errors, 'missing_planned_slots', 'ui_proof_slots', 'Planned UI proof input must include at least one slot.', 'Provide ui_proof_slots or no_ui_proof_rationale for non-UI work.');
345
+ return { valid: false, errors, warnings: [] };
346
+ }
347
+
348
+ for (const [index, slot] of normalizedSlots.entries()) {
349
+ const slotPath = `ui_proof_slots[${index}]`;
350
+ if (!isPlainObject(slot)) {
351
+ addError(errors, 'invalid_planned_slot', slotPath, 'Planned UI proof slot must be an object.', 'Record a scoped slot with claim, route_state, viewport, evidence, artifacts, validation, and claim limit.');
352
+ continue;
353
+ }
354
+ for (const field of REQUIRED_SLOT_FIELDS) requireField(slot, field, slotPath, errors);
355
+ for (const [kindIndex, kind] of normalizeArray(slot.required_evidence_kinds || slot.requiredEvidenceKinds).entries()) {
356
+ if (!EVIDENCE_KINDS.includes(kind)) {
357
+ addError(errors, 'unsupported_planned_evidence_kind', `${slotPath}.required_evidence_kinds[${kindIndex}]`, `Unsupported planned UI proof evidence kind: ${kind}`, `Use only: ${EVIDENCE_KINDS.join(', ')}.`);
358
+ }
359
+ }
360
+ if (hasValue(slot.manual_acceptance_required) && typeof slot.manual_acceptance_required !== 'boolean') {
361
+ addError(errors, 'invalid_manual_acceptance_required', `${slotPath}.manual_acceptance_required`, 'manual_acceptance_required must be a boolean.', 'Use true only when human judgment is required for this slot; otherwise use false.');
362
+ }
363
+ if (normalizeArray(slot.minimum_observations || slot.minimumObservations).length === 0) {
364
+ addError(errors, 'missing_minimum_observations', `${slotPath}.minimum_observations`, 'Planned UI proof slot must include minimum observations.', 'List the observations execution must prove for this slot.');
365
+ }
366
+ if (normalizeArray(slot.expected_artifact_types || slot.expectedArtifactTypes).length === 0) {
367
+ addError(errors, 'missing_expected_artifact_types', `${slotPath}.expected_artifact_types`, 'Planned UI proof slot must include expected artifact types.', 'List expected artifact types such as screenshot, trace, report, or dom_snapshot.');
368
+ }
369
+ }
370
+
371
+ return { valid: errors.length === 0, errors, warnings: [] };
372
+ }
373
+
280
374
  function validatePrivacy(bundle, errors, publicClaim) {
281
375
  validateObservationPrivacy(bundle.privacy, 'privacy', errors);
282
376
  if (publicClaim && bundle.privacy?.raw_artifacts_safe_to_publish !== true) {
@@ -539,15 +633,30 @@ function compareSlotToBundle(slot, slotIdValue, observed) {
539
633
  }
540
634
  }
541
635
 
636
+ const expectedArtifactTypes = normalizeArray(slot?.expected_artifact_types || slot?.expectedArtifactTypes);
637
+ const observedArtifactTypes = new Set(normalizeArray(bundle?.artifacts).filter(isPlainObject).flatMap((artifact) => [
638
+ typeof artifact.type === 'string' ? artifact.type.toLowerCase() : '',
639
+ artifactType(artifact),
640
+ ]).filter(Boolean));
641
+ const missingArtifactTypes = expectedArtifactTypes.filter((type) => !observedArtifactTypes.has(type));
642
+ if (missingArtifactTypes.length > 0) {
643
+ issues.push({
644
+ code: 'missing_expected_artifact_type',
645
+ path: 'artifacts[].type',
646
+ message: `Observed UI proof for slot ${slotIdValue} is missing expected artifact type(s): ${missingArtifactTypes.join(', ')}.`,
647
+ });
648
+ }
649
+
542
650
  const status = issues.length === 0 ? 'satisfied' : (bundleStatus === 'missing' ? 'missing' : 'partial');
543
651
  return { status, issues, source: observed.source };
544
652
  }
545
653
 
546
654
  export function compareUiProofSlots(plannedSlots, observedBundles) {
547
655
  const slots = normalizeArray(plannedSlots);
656
+ const slotValidation = validateUiProofSlots(slots);
548
657
  const bundles = normalizeArray(observedBundles).map(normalizeObservedBundle);
549
658
  const results = [];
550
- const errors = [];
659
+ const errors = [...slotValidation.errors];
551
660
 
552
661
  for (const observed of bundles) {
553
662
  if (!observed.validation.valid) {
@@ -614,14 +723,16 @@ export function validateUiProofBundle(bundle, options = {}) {
614
723
  const publicClaim = hasPublicClaim(bundle, options);
615
724
  validateClaimUses(bundle, options, errors);
616
725
  validateEvidenceKinds(bundle, errors);
726
+ validateToolsUsed(bundle, errors);
617
727
  validateCommandsOrManualSteps(bundle, errors);
618
728
  validateObservations(bundle, errors);
619
729
  validateResult(bundle, errors);
730
+ validateFailureClassification(bundle, errors);
620
731
  validateComparisonStatuses(bundle, errors);
621
732
  validateClaimLimits(bundle, errors);
622
733
  validatePrivacy(bundle, errors, publicClaim);
623
734
  validatePublicObservationPrivacy(bundle, errors, publicClaim);
624
- const artifactRefs = validateArtifacts(bundle, errors, publicClaim);
735
+ const artifactRefs = validateArtifacts(bundle, errors, publicClaim, options);
625
736
  validateObservationArtifactRefs(bundle, artifactRefs, errors);
626
737
 
627
738
  return { valid: errors.length === 0, errors, warnings };
@@ -688,7 +799,14 @@ export function parseUiProofSlotsContent(content, filePath = 'UI proof slots') {
688
799
  };
689
800
  }
690
801
 
691
- return { slots, errors: [] };
802
+ const validation = validateUiProofSlots(slots);
803
+ return {
804
+ slots,
805
+ errors: validation.errors.map((error) => ({
806
+ ...error,
807
+ path: error.path === 'ui_proof_slots' ? filePath : `${filePath}.${error.path}`,
808
+ })),
809
+ };
692
810
  }
693
811
 
694
812
  export function readUiProofBundleFile(filePath) {
@@ -763,7 +881,12 @@ function cmdValidate(cwd, args) {
763
881
  const parsed = readUiProofBundleFile(target);
764
882
  const validation = parsed.errors.length > 0
765
883
  ? { valid: false, errors: parsed.errors, warnings: [] }
766
- : validateUiProofBundle(parsed.bundle, { claimUses: parseClaimUse(flags) });
884
+ : validateUiProofBundle(parsed.bundle, {
885
+ claimUses: parseClaimUse(flags),
886
+ requireLocalArtifactExists: true,
887
+ workspaceRoot: cwd,
888
+ bundleDir: dirname(target),
889
+ });
767
890
 
768
891
  output({ operation: 'ui-proof validate', target: targetArg, valid: validation.valid, errors: validation.errors, warnings: validation.warnings });
769
892
  if (!validation.valid) process.exitCode = 1;
@@ -799,7 +922,7 @@ function cmdCompare(cwd, args) {
799
922
  }
800
923
 
801
924
  const comparison = planned.errors.length > 0
802
- ? { status: 'missing', slots: [], errors: planned.errors }
925
+ ? { status: 'partial', slots: [], errors: planned.errors }
803
926
  : compareUiProofSlots(planned.slots, observedBundles);
804
927
 
805
928
  output({
@@ -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
 
@@ -2817,30 +2818,36 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru
2817
2818
 
2818
2819
  ## D62 - Repo-Native UI Proof Contract
2819
2820
 
2820
- **Decision (2026-04-28):** UI-sensitive work should carry a compact planned proof-slot contract and, when executed, an observed UI proof bundle that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`.
2821
+ **Decision (2026-04-28; revised 2026-05-08):** UI-sensitive work should carry a compact planned proof-slot contract and, when executed, an observed UI proof bundle that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`. For live rendered UI proof, `agent-browser` is the default runtime evidence path for consumers, while existing Playwright tests remain the canonical repeatable browser-regression path when present. The deterministic `ui-proof` validator remains provider-agnostic structural validation, but it now validates planned slot specificity, concise tool provenance, local artifact path existence when validating from files, raw-artifact safety for paths and URLs, and failed/partial proof classification so the workflow cannot degrade back into unstructured "looks good" review.
2821
2822
 
2822
2823
  **Context:**
2823
2824
  - UI proof targets the recurring failure mode where agents claim a UI works or looks good without rendered proof, matched observations, or explicit human judgment.
2824
2825
  - The contract defines proof slots, proof bundles, comparison statuses, fail-closed agent guardrails, deterministic metadata validation, privacy metadata, and health visibility without adding a browser-provider framework.
2825
2826
  - GSD's archived planner, executor, and verifier roles preserve strong lifecycle discipline, but they do not provide this UI-specific planned-vs-observed proof model. GSDD keeps the lifecycle leverage and adds a repo-native UI proof substrate without adding a browser-provider framework.
2827
+ - OneShot's QC guidance and Vercel's `agent-browser` skill converge on an interactive browser loop for snapshots, ref-based interaction, screenshots, and network/console-adjacent inspection. GSDD adapts that as a default workflow instruction, not as a hard validator dependency.
2826
2828
 
2827
2829
  **Decision:**
2828
2830
  - Planning must classify UI-sensitive work and require either `ui_proof_slots` or an explicit `no_ui_proof_rationale`.
2829
- - Planned slots record claim, route/state, required evidence kinds, minimum observations, environment/viewport, manual-acceptance requirement, claim limit, and requirement IDs.
2831
+ - Planned slots record claim, route/state, required evidence kinds, minimum observations, expected artifact types, runnable validation command, environment/viewport, manual-acceptance requirement, claim limit, and requirement IDs.
2830
2832
  - Observed proof bundles record claim, requirement/slot IDs, route/state, environment, viewport, evidence inputs, commands/manual steps, observations, artifacts, privacy metadata, result, and claim limits.
2833
+ - Planned slots must be tight enough for the plan checker to reject vague proof: specific route/state, viewport rationale or narrowed claim limit, minimum observations, expected artifact types, runnable validation, and matchability back to the exact UI claim.
2834
+ - The planner chooses viewport coverage, but responsive or layout-sensitive claims require desktop/mobile or equivalent state coverage unless the claim is explicitly narrowed.
2835
+ - Execution defaults to `agent-browser` for live UI runtime proof: open the route/state, capture interactive snapshots/refs where relevant, exercise the changed flow, capture screenshots for planned viewport(s), and record relevant console/network observations.
2836
+ - Existing Playwright tests or package scripts remain the canonical repeatable browser-regression evidence when present. Playwright scripting is reserved for checks `agent-browser` cannot cover cleanly, such as JS-disabled behavior, structured console listeners, or multi-context testing.
2831
2837
  - Verification compares planned slots to observed bundles using `satisfied`, `partial`, `missing`, `waived`, `deferred`, and `not_applicable`; waiver and deferral are not proof.
2832
2838
  - UI correctness claims fail closed unless rendered proof is matched exactly to claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit, or an explicit waiver/deferment narrows the claim.
2833
2839
  - Human acceptance may close a narrowed claim and record proof debt, but it must not convert missing or mismatched non-human evidence into `satisfied` proof.
2834
2840
  - Screenshots, traces, videos, reports, accessibility scans, Gherkin, and visual diffs are artifact types or activities mapped onto the five existing evidence kinds, not new evidence kinds.
2835
2841
  - Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may discover proof obligations, but they are discovery hints only and do not satisfy proof slots.
2836
2842
  - Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver, and human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence.
2837
- - Deterministic metadata enforcement keeps the evidence and comparison-status vocabularies unchanged: artifact entries require `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to `local_only` plus `safe_to_publish: false`; `bin/lib/ui-proof.mjs` validates required bundle/observation fields, structured command/manual-step entries, fixed evidence kinds, claim/result statuses, comparison statuses, claim limits, privacy metadata, safe artifact references, and public/tracked/delivery proof claims backed by local-only, unsafe, unsanitized, or privacy-contradictory artifacts.
2838
- - `gsdd health` reports invalid known UI proof bundles as E10 using the same validator, staying read-only and metadata-only.
2843
+ - Deterministic validation keeps the evidence and comparison-status vocabularies unchanged: planned slots require specific claim, route/state, evidence, expected artifacts, validation, viewport, and claim-limit fields; artifact entries require `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to `local_only` plus `safe_to_publish: false`; `bin/lib/ui-proof.mjs` validates required bundle/observation fields, structured command/manual-step entries, fixed evidence kinds, concise `tools_used` IDs, claim/result statuses, comparison statuses, failure classification for failed/partial proof, claim limits, privacy metadata, safe artifact references, local artifact path existence when validating file-backed bundles, and public/tracked/delivery proof claims backed by local-only, unsafe, unsanitized, or privacy-contradictory artifacts.
2844
+ - `gsdd health` reports invalid known UI proof bundles as E10 using the same validator, staying read-only and avoiding raw artifact content inspection.
2845
+ - Failed UI proof is reported through existing GSDD gap/proof-debt language. Product behavior defects, missing or blocked infrastructure, flaky harnesses, and ambiguous specs explain causes, but they do not add new evidence kinds, result statuses, or comparison statuses.
2839
2846
 
2840
2847
  **Leverage:**
2841
- - Lost: UI-sensitive work now carries a small proof-contract burden, and invalid proof metadata can degrade/break health before agents can claim rendered UI outcomes.
2842
- - Kept: repo-native markdown artifacts, optional project tooling, fixed closure evidence kinds, generated-surface freshness, and the plan/execute/verify separation.
2843
- - Gained: exact claim-to-proof traceability, strict comparison statuses, privacy and claim-limit metadata, fail-closed overclaim guardrails, deterministic metadata validation, and health-visible protection against unsafe public proof claims.
2848
+ - Lost: UI-sensitive work now carries a small proof-contract burden, and default live proof guidance adds slightly more specificity for planners/checkers to enforce.
2849
+ - Kept: repo-native markdown artifacts, optional project tooling, fixed closure evidence kinds, generated-surface freshness, the plan/execute/verify separation, and provider-agnostic deterministic metadata validation.
2850
+ - Gained: exact claim-to-proof traceability, strict comparison statuses, privacy and claim-limit metadata, fail-closed overclaim guardrails, deterministic metadata validation, a concrete live browser evidence path, and health-visible protection against unsafe public proof claims.
2844
2851
 
2845
2852
  **Evidence:**
2846
2853
  - `distilled/templates/ui-proof.md`
@@ -2849,11 +2856,60 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru
2849
2856
  - `bin/lib/templates.mjs`, `bin/lib/ui-proof.mjs`, `bin/lib/health.mjs`, `bin/lib/rendering.mjs`
2850
2857
  - `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.health.test.cjs`, `tests/gsdd.init.test.cjs`
2851
2858
  - GSD comparison: the upstream planner, executor, and verifier role patterns preserve lifecycle rigor, but they do not define UI proof slots or planned-vs-observed UI proof bundles.
2859
+ - OneShot QC source: `https://github.com/oneshot-repo/OneShot/tree/main/skills`
2860
+ - Vercel `agent-browser` docs: `https://github.com/vercel-labs/agent-browser/blob/main/skill-data/core/SKILL.md` and `https://agent-browser.dev/snapshots`
2861
+ - Playwright docs: `https://playwright.dev/docs/trace-viewer`, `https://playwright.dev/docs/next/screenshots`, and `https://playwright.dev/mcp/tools/tracing`
2862
+ - Chrome DevTools for agents/MCP docs: `https://developer.chrome.com/docs/devtools/agents` and `https://developer.chrome.com/blog/chrome-devtools-mcp?hl=en`
2863
+ - Harness and UI-agent pitfall sources: `https://www.anthropic.com/engineering/harness-design-long-running-apps`, `https://www.huuhka.net/browser-verification-for-coding-agents-chrome-devtools-mcp-vs-agent-browser/`, `https://www.developersdigest.tech/blog/long-running-agents-need-harnesses`, `https://codemyspec.com/blog/agentic-qa-verification`, `https://dev.to/ratikkoka/your-ui-is-invisible-to-ai-agents-heres-how-to-fix-it-1ib3`, `https://dev.to/louaiboumediene/the-ai-harness-why-your-ai-coding-agent-is-only-as-smart-as-the-repo-you-put-it-in-cml`, and `https://tessl.io/blog/webmcp-making-web-apps-faster-and-cheaper-for-ai-agents/`
2864
+ - OpenSpec docs: `https://openspec.dev/`
2865
+ - LeanSpec docs: `https://www.lean-spec.dev/docs/guide/first-principles`
2866
+ - OpenAI Codex docs: `https://help.openai.com/en/articles/11369540-codex-in-chatgpt`
2867
+ - Anthropic Agent Skills docs: `https://docs.claude.com/en/docs/agents-and-tools/agent-skills`
2868
+ - GitHub Copilot customization docs: `https://docs.github.com/en/copilot/concepts/prompting/response-customization`
2852
2869
 
2853
2870
  **Consequences:**
2854
2871
  - Future UI-related phases must not add new evidence kinds by treating artifact types as proof categories.
2855
2872
  - Future dogfood or runtime validation must not upgrade artifact counts or human waivers into proof.
2856
2873
  - Generated runtime surfaces and local templates must stay freshness-checkable through `gsdd update --templates` and health diagnostics.
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.
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.
2857
2913
 
2858
2914
  ---
2859
2915
 
@@ -491,6 +491,29 @@
491
491
  - `agents/planner.md`, `agents/executor.md`, `agents/verifier.md`, `distilled/templates/delegates/plan-checker.md`
492
492
  - `bin/lib/templates.mjs`, `bin/lib/ui-proof.mjs`, `bin/lib/health.mjs`, `bin/lib/rendering.mjs`
493
493
  - `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.health.test.cjs`, `tests/gsdd.init.test.cjs`
494
+ - OneShot QC/browser policy: https://github.com/oneshot-repo/OneShot/tree/main/skills
495
+ - Vercel agent-browser docs: https://github.com/vercel-labs/agent-browser/blob/main/skill-data/core/SKILL.md, https://agent-browser.dev/snapshots
496
+ - Playwright browser proof docs: https://playwright.dev/docs/trace-viewer, https://playwright.dev/docs/next/screenshots, https://playwright.dev/mcp/tools/tracing
497
+ - Chrome DevTools agent/browser docs: https://developer.chrome.com/docs/devtools/agents, https://developer.chrome.com/blog/chrome-devtools-mcp?hl=en
498
+ - Harness engineering source: https://www.anthropic.com/engineering/harness-design-long-running-apps
499
+ - UI-agent browser verification source: https://www.huuhka.net/browser-verification-for-coding-agents-chrome-devtools-mcp-vs-agent-browser/
500
+ - Agent harness discipline source: https://www.developersdigest.tech/blog/long-running-agents-need-harnesses
501
+ - Agentic QA verification-gap source: https://codemyspec.com/blog/agentic-qa-verification
502
+ - Agent-hostile UI semantics source: https://dev.to/ratikkoka/your-ui-is-invisible-to-ai-agents-heres-how-to-fix-it-1ib3
503
+ - Browser-harness efficiency source: https://dev.to/louaiboumediene/the-ai-harness-why-your-ai-coding-agent-is-only-as-smart-as-the-repo-you-put-it-in-cml
504
+ - Dynamic-UI harness brittleness source: https://tessl.io/blog/webmcp-making-web-apps-faster-and-cheaper-for-ai-agents/
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
+ - 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
+
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
494
517
 
495
518
  ---
496
519
 
@@ -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]`.
@@ -35,6 +35,7 @@ Verify these dimensions:
35
35
  - `escalation_integrity`: tasks include checkpoints or escalation when evidence, permissions, user decisions, or risky ambiguity are required.
36
36
  - `closure_honesty`: the plan's done criteria and evidence limits support only claims that execution can actually prove.
37
37
  - `closure_honesty`: for UI proof, reject agent-only `looks good` closure, artifact-count proof, unsupported evidence kinds, and human acceptance that converts missing/mismatched non-human evidence into `satisfied` proof. Waiver, deferment, proof debt, or narrowed-claim language is acceptable only when the stronger UI claim is not treated as proven.
38
+ - `closure_honesty`: for UI proof planning, reject weak slots that omit a specific route/state, viewport rationale or narrowed viewport claim limit, minimum observations, expected artifact types, runnable validation, or a way to compare observed proof back to the planned claim. Treat under-specified viewport coverage as a blocker for responsive or layout-sensitive claims. `agent-browser` is the default live runtime evidence path; do not block a slot solely for using another project-native browser path, but require the plan to explain the `agent-browser` availability constraint and fallback choice.
38
39
  - `closure_honesty`: for UI proof privacy, require artifact `visibility`, `retention`, `sensitivity`, and `safe_to_publish`, require `gsdd ui-proof validate` or `gsdd health` when bundle metadata exists, and reject public/tracked/delivery/publication proof claims backed by local-only or `safe_to_publish: false` artifacts.
39
40
  - `high_leverage_review`: high-leverage surfaces have a second-pass review or equivalent contradiction/staleness check before completion.
40
41
  - `approach_alignment`: when APPROACH.md is provided, verify that plan tasks implement the chosen approaches from the user's decisions. Check: