@opengsd/gsd-core 1.7.0-rc.5 → 1.7.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.
Files changed (112) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/agents/gsd-ui-checker.md +2 -0
  6. package/agents/gsd-ui-researcher.md +1 -0
  7. package/bin/install.js +975 -196
  8. package/commands/gsd/mempalace-capture.md +27 -1
  9. package/commands/gsd/surface.md +6 -6
  10. package/gsd-core/bin/gsd-tools.cjs +63 -2
  11. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  12. package/gsd-core/bin/lib/audit.cjs +7 -6
  13. package/gsd-core/bin/lib/capability-registry.cjs +503 -87
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  16. package/gsd-core/bin/lib/clock.cjs +19 -0
  17. package/gsd-core/bin/lib/commands.cjs +48 -9
  18. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  19. package/gsd-core/bin/lib/config.cjs +12 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  21. package/gsd-core/bin/lib/drift.cjs +4 -4
  22. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  23. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  24. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  25. package/gsd-core/bin/lib/init.cjs +60 -53
  26. package/gsd-core/bin/lib/install-engine.cjs +93 -22
  27. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  28. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  29. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  31. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  32. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  33. package/gsd-core/bin/lib/milestone.cjs +217 -31
  34. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  35. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  36. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  37. package/gsd-core/bin/lib/phase.cjs +436 -61
  38. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  39. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  40. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  41. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
  43. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  44. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
  45. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  46. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  47. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
  48. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  49. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  50. package/gsd-core/bin/lib/security.cjs +7 -37
  51. package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
  52. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  53. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  54. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  55. package/gsd-core/bin/lib/state.cjs +391 -126
  56. package/gsd-core/bin/lib/surface.cjs +12 -8
  57. package/gsd-core/bin/lib/template.cjs +2 -1
  58. package/gsd-core/bin/lib/uat.cjs +54 -8
  59. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  60. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  61. package/gsd-core/bin/lib/verify.cjs +4 -3
  62. package/gsd-core/bin/lib/workstream.cjs +3 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  64. package/gsd-core/bin/lib/write-set.cjs +38 -0
  65. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  66. package/gsd-core/bin/shared/model-catalog.json +8 -3
  67. package/gsd-core/references/checkpoints.md +12 -0
  68. package/gsd-core/references/ui-consideration-probe.md +73 -0
  69. package/gsd-core/templates/UI-SPEC.md +25 -0
  70. package/gsd-core/templates/VALIDATION.md +2 -0
  71. package/gsd-core/workflows/add-tests.md +1 -1
  72. package/gsd-core/workflows/audit-milestone.md +7 -4
  73. package/gsd-core/workflows/debug.md +2 -0
  74. package/gsd-core/workflows/execute-phase.md +5 -3
  75. package/gsd-core/workflows/fast.md +8 -22
  76. package/gsd-core/workflows/plan-phase.md +6 -0
  77. package/gsd-core/workflows/progress.md +2 -2
  78. package/gsd-core/workflows/quick.md +2 -0
  79. package/gsd-core/workflows/review.md +42 -3
  80. package/gsd-core/workflows/secure-phase.md +1 -1
  81. package/gsd-core/workflows/settings-advanced.md +7 -4
  82. package/gsd-core/workflows/ship.md +8 -2
  83. package/gsd-core/workflows/spec-phase.md +1 -1
  84. package/gsd-core/workflows/transition.md +1 -1
  85. package/gsd-core/workflows/ui-phase.md +146 -1
  86. package/gsd-core/workflows/validate-phase.md +2 -2
  87. package/hooks/dist/gsd-statusline.js +164 -14
  88. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  89. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  90. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  91. package/hooks/gsd-statusline.js +164 -14
  92. package/hooks/gsd-windsurf-pre-command.js +275 -0
  93. package/hooks/gsd-windsurf-pre-write.js +132 -0
  94. package/hooks/managed-hooks-registry.cjs +2 -0
  95. package/package.json +10 -4
  96. package/pi/gsd.cjs +354 -0
  97. package/scripts/build-hooks.js +3 -0
  98. package/scripts/ci-test-scope.cjs +39 -1
  99. package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
  100. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  101. package/scripts/gen-registry.cjs +128 -0
  102. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  103. package/scripts/lint-table-schema-drift.cjs +157 -0
  104. package/scripts/lint-test-file-count.allowlist.json +2 -1
  105. package/scripts/registry-schema.cjs +565 -0
  106. package/scripts/validate-registry.cjs +117 -0
  107. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  108. package/skills/gsd-surface/SKILL.md +6 -6
  109. package/vscode/browser.js +197 -0
  110. package/vscode/extension.js +383 -0
  111. package/vscode/host-binding.js +113 -0
  112. package/vscode/package.json +96 -0
@@ -28,7 +28,7 @@ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
28
28
  const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
29
29
  // eslint-disable-next-line @typescript-eslint/no-require-imports
30
30
  const phaseIdMod = require("./phase-id.cjs");
31
- const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
31
+ const { extractFrontmatter, reconstructFrontmatter, stripFrontmatter } = frontmatter;
32
32
  const { escapeRegex } = phaseIdMod;
33
33
  // Stop predicate for section-body slicing: a level-2+ heading ends the section.
34
34
  const STOP_H2_PLUS = (lv) => lv >= 2;
@@ -226,7 +226,7 @@ function beginPhaseCore(content, intent, deps) {
226
226
  const reassemble = (b) => hasFrontmatter
227
227
  ? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}`
228
228
  : b;
229
- const today = deps.clock.today();
229
+ const today = deps.clock.localToday();
230
230
  // Consult the field-classification table for the frontmatter keys this
231
231
  // transition touches (codex Phase 1 review: "table not consulted by
232
232
  // transitionCore"). The table tracks FRONTMATTER keys (lowercase: `status`,
@@ -284,8 +284,8 @@ function beginPhaseCore(content, intent, deps) {
284
284
  updated.push('Current focus');
285
285
  }
286
286
  // ## Current Position section mutation (#1104, #1365).
287
- // ADR-1372 T6: tokenizeHeadings + offset splicing (replaceSection adoption
288
- // deferred to a later phase). Mirrors state.cts:2261-2324 byte-for-behaviour.
287
+ // `locateCurrentPosition` (fence-aware, tokenizeHeadings-based) locates
288
+ // the section; mirrors state.cts:2261-2324 byte-for-behaviour.
289
289
  body = mutateCurrentPositionFirstTime(body, intent, today, updated);
290
290
  }
291
291
  else {
@@ -338,6 +338,20 @@ function sliceCurrentPositionSection(body) {
338
338
  * First-time ## Current Position mutation: update Phase / Plan / Status /
339
339
  * Last activity lines. Mirrors state.cts:2261-2324 byte-for-behaviour
340
340
  * (inline regex first, pipe-table fallback via stateReplaceField — #1257).
341
+ *
342
+ * F2 (#2245 review, MAJOR): a prior revision of this function used
343
+ * `collectSection`/`replaceSection` here, whose default `levelBounded: true`
344
+ * only stops the section at the next heading of level <= the opener's own
345
+ * level (H1/H2 for a `##`-opened section) — an H3+ subsection nested under
346
+ * `## Current Position` was NOT a stop boundary and got folded into
347
+ * `sectionBody`, so the field regexes below (which run with the `m` flag,
348
+ * matching ANY line start in the body) could clobber a same-named line
349
+ * inside that subsection (the #2130/#2067/#2080 truncation/clobber class).
350
+ * Restored to the fence-aware `locateCurrentPosition` locator (which stops
351
+ * at ANY heading level >= 2, `STOP_H2_PLUS` — H2 through H6) + manual splice,
352
+ * exactly matching the `mutateCurrentPositionResume`/
353
+ * `mutateCurrentPositionForAdvance` siblings below, both of which use
354
+ * `locateCurrentPosition` directly.
341
355
  */
342
356
  function mutateCurrentPositionFirstTime(body, intent, today, updated) {
343
357
  const span = locateCurrentPosition(body);
@@ -414,25 +428,6 @@ function mutateCurrentPositionResume(body, intent, today, updated) {
414
428
  }
415
429
  return body.slice(0, span.start) + sectionBody + body.slice(span.end);
416
430
  }
417
- /**
418
- * Strip ALL frontmatter blocks from the start of `content`.
419
- *
420
- * TODO (ADR-1769 follow-up): move to `frontmatter.cjs` or `state-document.cjs`
421
- * so it's a shared primitive. Inlined here in Phase 1 to avoid touching
422
- * `state.cjs` (which is the migration target itself) and to keep the Phase 1
423
- * diff contained. Body is byte-identical to `state.cts:1653 stripFrontmatter`
424
- * (same CRLF + stacked-block handling).
425
- */
426
- function stripFrontmatter(content) {
427
- let result = content;
428
- while (true) {
429
- const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
430
- if (stripped === result)
431
- break;
432
- result = stripped;
433
- }
434
- return result;
435
- }
436
431
  /**
437
432
  * Update fields within the ## Current Position section for advancePlan.
438
433
  * Mirrors `updateCurrentPositionFields` (state.cts:496) byte-for-behaviour:
@@ -502,7 +497,7 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
502
497
  * adapter to construct CLI output.
503
498
  */
504
499
  function advancePlanCore(content, deps) {
505
- const today = deps.clock.today();
500
+ const today = deps.clock.localToday();
506
501
  // #1255: body-field replacements operate on body only (frontmatter stripped),
507
502
  // not on the full content. The YAML `status:` key matches `^Status:\s*`
508
503
  // before the body field if full content is passed (codex Phase 2 review:
@@ -591,8 +586,8 @@ function advancePlanCore(content, deps) {
591
586
  * Migrates the inline STATE.md transform that lived inside `cmdPhaseComplete`
592
587
  * (phase.cts) onto the substrate. Owns the field-classification-governed body
593
588
  * mutations: Current Phase (preserving the `of total` shape and phase name),
594
- * Current Phase Name, Status (`Milestone complete` on the last phase, else
595
- * `Ready to plan`), Current Plan (`Not started`), Last Activity + Description,
589
+ * Current Phase Name, Status (`All phases complete` on the last phase, else
590
+ * `Ready to plan` per ADR-2207), Current Plan (`Not started`), Last Activity + Description,
596
591
  * and the Completed/Total Phases + Progress percent block (re-derived from the
597
592
  * roadmap via the injected `roadmapProvider`).
598
593
  *
@@ -608,7 +603,7 @@ function advancePlanCore(content, deps) {
608
603
  */
609
604
  function completePhaseCore(content, intent, deps) {
610
605
  const updated = [];
611
- const today = deps.clock.today();
606
+ const today = deps.clock.localToday();
612
607
  // Consult the field-classification table for the frontmatter keys this
613
608
  // transition touches (same guard beginPhaseCore applies). A missing row is a
614
609
  // substrate defect — fail loudly rather than silently re-encoding policy.
@@ -672,8 +667,10 @@ function completePhaseCore(content, intent, deps) {
672
667
  updated.push('Current Phase Name');
673
668
  }
674
669
  }
675
- // Status — `Milestone complete` on the final phase, otherwise `Ready to plan`.
676
- const statusValue = intent.isLastPhase ? 'Milestone complete' : 'Ready to plan';
670
+ // Status — `All phases complete` on the final phase (ADR-2207), otherwise
671
+ // `Ready to plan`. Milestone termination (`<version> milestone complete`) is
672
+ // owned solely by the milestone-close verb (milestoneCompleteCore).
673
+ const statusValue = intent.isLastPhase ? 'All phases complete' : 'Ready to plan';
677
674
  const statusAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Status', null, statusValue);
678
675
  if (statusAfter !== body) {
679
676
  body = statusAfter;
@@ -762,7 +759,7 @@ function completePhaseCore(content, intent, deps) {
762
759
  */
763
760
  function plannedPhaseCore(content, intent, deps) {
764
761
  const updated = [];
765
- const today = deps.clock.today();
762
+ const today = deps.clock.localToday();
766
763
  for (const fmKey of ['status', 'last_activity', 'last_activity_desc']) {
767
764
  const cls = getFieldClassification(fmKey);
768
765
  if (cls === null) {
@@ -840,7 +837,7 @@ function plannedPhaseCore(content, intent, deps) {
840
837
  * directly and must not run the steady-state `syncStateFrontmatter` post-sync.
841
838
  */
842
839
  function milestoneSwitchCore(content, intent, deps) {
843
- const today = deps.clock.today();
840
+ const today = deps.clock.localToday();
844
841
  const updated = [
845
842
  'milestone',
846
843
  'milestone_name',
@@ -903,6 +900,70 @@ function milestoneSwitchCore(content, intent, deps) {
903
900
  // ----------------------------------------------------------------------------
904
901
  // milestoneComplete — intent implementation (Phase 5)
905
902
  // ----------------------------------------------------------------------------
903
+ /**
904
+ * Replace a section's ENTIRE body with `newBody`, discarding whatever was
905
+ * there — the "wholesale reset" write pattern used by milestoneComplete's
906
+ * closure write (## Current Position / ## Operator Next Steps). Retires the
907
+ * fence-blind raw regex `(##\s*<heading>\s*\n)([\s\S]*?)(?=\n##|$)`, which a
908
+ * literal `##` inside a fenced code block in the section body could fool into
909
+ * stopping early (the #2130/#2067/#2080 truncation class) — heading location
910
+ * here goes through `tokenizeHeadings`, which is fence-aware.
911
+ *
912
+ * Byte-parity note: the retired regex's greedy `\s*` (before its mandatory
913
+ * `\n`) swallowed any blank line(s) immediately after the heading into the
914
+ * discarded match, and its non-greedy body match always left exactly ONE
915
+ * newline unconsumed before the next heading (or EOF), regardless of how many
916
+ * blank lines originally separated the section from what followed. Both
917
+ * edges are reproduced explicitly (rather than delegated to `collectSection`'s
918
+ * `trimEnd()`-based body, which trims a *different* amount and would drift
919
+ * the surrounding blank-line count) so `newBody`'s own leading/trailing
920
+ * formatting is exactly what appears in the output.
921
+ *
922
+ * Returns `null` when no heading matches `headingPredicate` (mirrors the
923
+ * retired regex's `pattern.test(body)` miss) — callers fall back to their own
924
+ * append-a-new-section path.
925
+ */
926
+ function resetSectionVerbatim(content, headingPredicate, newBody) {
927
+ const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
928
+ const idx = headings.findIndex(headingPredicate);
929
+ if (idx === -1)
930
+ return null;
931
+ const target = headings[idx];
932
+ const lines = content.split('\n');
933
+ const headingLineEnd = target.offset + lines[target.line - 1].length + 1;
934
+ // Swallow blank line(s) immediately after the heading (mirrors the retired
935
+ // regex's greedy `\s*` folding them into the discarded match).
936
+ //
937
+ // F7 (#2245 review, nit): recognise a CRLF blank line (`\r\n`), not only a
938
+ // bare LF — a lone `content[bodyStart] === '\n'` check never advances past
939
+ // a `\r` byte, so on a CRLF STATE.md the blank line right after the
940
+ // heading fell into the DISCARDED [bodyStart, bodyEnd) span instead of the
941
+ // KEPT prefix, silently dropping one blank line (contradicting this
942
+ // function's own byte-parity docstring).
943
+ let bodyStart = headingLineEnd;
944
+ while (bodyStart < content.length) {
945
+ if (content[bodyStart] === '\n') {
946
+ bodyStart += 1;
947
+ continue;
948
+ }
949
+ if (content[bodyStart] === '\r' && content[bodyStart + 1] === '\n') {
950
+ bodyStart += 2;
951
+ continue;
952
+ }
953
+ break;
954
+ }
955
+ // Stop at the next heading of level >= 2 (mirrors the retired regex's
956
+ // literal `##` lookahead, which matches any ATX heading two-or-more levels
957
+ // deep); leave exactly one newline unconsumed before it, or run to EOF.
958
+ let bodyEnd = content.length;
959
+ for (let j = idx + 1; j < headings.length; j++) {
960
+ if (STOP_H2_PLUS(headings[j].level)) {
961
+ bodyEnd = headings[j].offset - 1;
962
+ break;
963
+ }
964
+ }
965
+ return content.slice(0, bodyStart) + newBody + content.slice(bodyEnd);
966
+ }
906
967
  /**
907
968
  * Apply a `milestoneComplete` transition to STATE.md content.
908
969
  *
@@ -917,17 +978,11 @@ function milestoneSwitchCore(content, intent, deps) {
917
978
  * runtime-specific next-milestone slash command, injecting it via
918
979
  * `intent.nextMilestoneCommand` so the core stays pure.
919
980
  *
920
- * The two section resets use raw regex (with the pre-seam `allow-adhoc-markdown`
921
- * waivers carried from milestone.cts) rather than tokenizeHeadings because the
922
- * `## Operator Next Steps` section is non-canonical (not in STATE_MD_SECTIONS)
923
- * and the existing behavior + its tests pin the exact regex semantics. A future
924
- * collectSection migration (#1372) can swap both to section primitives.
925
- *
926
981
  * Behavior is byte-for-byte with the pre-migration milestone.cts:314-353 block.
927
982
  */
928
983
  function milestoneCompleteCore(content, intent, deps) {
929
984
  const updated = [];
930
- const today = deps.clock.today();
985
+ const today = deps.clock.localToday();
931
986
  const version = intent.version;
932
987
  for (const fmKey of ['status', 'last_activity', 'last_activity_desc']) {
933
988
  const cls = getFieldClassification(fmKey);
@@ -963,22 +1018,22 @@ function milestoneCompleteCore(content, intent, deps) {
963
1018
  }
964
1019
  // ## Current Position reset — stop resume/progress flows pointing at closed
965
1020
  // execution instructions.
966
- const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify carried from milestone.cts; pending collectSection migration #1372
967
1021
  const closedPositionBody = `\nPhase: Milestone ${version} complete\n` +
968
1022
  `Plan: —\n` +
969
1023
  `Status: Awaiting next milestone\n` +
970
1024
  `Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
971
- if (positionPattern.test(body)) {
972
- body = body.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`);
1025
+ const positionReset = resetSectionVerbatim(body, (h) => h.level === 2 && /^current\s+position$/i.test(h.text), closedPositionBody);
1026
+ if (positionReset !== null) {
1027
+ body = positionReset;
973
1028
  }
974
1029
  else {
975
1030
  body = `${body.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
976
1031
  }
977
1032
  updated.push('Current Position');
978
1033
  // ## Operator Next Steps — normalize stale tails that can persist after close.
979
- const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify carried from milestone.cts; pending collectSection migration #1372
980
- if (operatorPattern.test(body)) {
981
- body = body.replace(operatorPattern, `$1\n- Start the next milestone with ${intent.nextMilestoneCommand}\n\n`);
1034
+ const operatorReset = resetSectionVerbatim(body, (h) => h.level === 2 && /^operator\s+next\s+steps$/i.test(h.text), `\n- Start the next milestone with ${intent.nextMilestoneCommand}\n\n`);
1035
+ if (operatorReset !== null) {
1036
+ body = operatorReset;
982
1037
  }
983
1038
  else {
984
1039
  body = `${body.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with ${intent.nextMilestoneCommand}\n`;
@@ -1175,7 +1230,7 @@ function pruneCore(content, intent) {
1175
1230
  * (rather than silently writing fallback-derived wrong values).
1176
1231
  */
1177
1232
  function syncCore(content, intent, deps) {
1178
- const today = deps.clock.today();
1233
+ const today = deps.clock.localToday();
1179
1234
  const changes = [];
1180
1235
  let modified = content;
1181
1236
  const updated = [];