@opengsd/gsd-core 1.20.1 → 1.21.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 (135) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +234 -126
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/gsd-core/bin/gsd-tools.cjs +450 -753
  11. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  12. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  13. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  14. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  15. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  16. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  17. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  18. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  19. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  20. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  21. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  22. package/gsd-core/bin/lib/commands.cjs +80 -2
  23. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  24. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  25. package/gsd-core/bin/lib/config.cjs +139 -101
  26. package/gsd-core/bin/lib/configuration.cjs +41 -2
  27. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  28. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  29. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  30. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  33. package/gsd-core/bin/lib/graphify.cjs +19 -2
  34. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  35. package/gsd-core/bin/lib/init.cjs +13 -11
  36. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  37. package/gsd-core/bin/lib/install-engine.cjs +11 -19
  38. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  39. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  40. package/gsd-core/bin/lib/io.cjs +6 -0
  41. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  42. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  43. package/gsd-core/bin/lib/milestone.cjs +8 -6
  44. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  45. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  46. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  47. package/gsd-core/bin/lib/phase.cjs +107 -31
  48. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  49. package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
  50. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  51. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  52. package/gsd-core/bin/lib/project-context.cjs +439 -0
  53. package/gsd-core/bin/lib/project-root.cjs +9 -230
  54. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  55. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  56. package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
  57. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  58. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  59. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
  60. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  61. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  62. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  63. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  64. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  65. package/gsd-core/bin/lib/state-transition.cjs +91 -41
  66. package/gsd-core/bin/lib/state.cjs +446 -61
  67. package/gsd-core/bin/lib/surface.cjs +4 -7
  68. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  69. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  70. package/gsd-core/bin/lib/verify.cjs +49 -6
  71. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  73. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  74. package/gsd-core/references/checkpoints.md +1 -1
  75. package/gsd-core/references/planner-guidance.md +11 -0
  76. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  77. package/gsd-core/references/planner-reversibility.md +2 -0
  78. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  79. package/gsd-core/workflows/autonomous.md +4 -4
  80. package/gsd-core/workflows/code-review-fix.md +5 -7
  81. package/gsd-core/workflows/execute-phase.md +21 -7
  82. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  83. package/gsd-core/workflows/plan-phase.md +18 -19
  84. package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
  85. package/hooks/dist/gsd-config-reload.js +2 -2
  86. package/hooks/dist/gsd-context-monitor.js +2 -1
  87. package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
  88. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  89. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-workflow-guard.js +2 -1
  92. package/hooks/dist/lib/config-read.js +49 -0
  93. package/hooks/dist/lib/git-cmd.js +3 -1
  94. package/hooks/dist/lib/git-head-tree.js +474 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +97 -17
  96. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  97. package/hooks/gsd-agent-isolation-guard.js +3 -2
  98. package/hooks/gsd-config-reload.js +2 -2
  99. package/hooks/gsd-context-monitor.js +2 -1
  100. package/hooks/gsd-cursor-subagent-start.js +8 -5
  101. package/hooks/gsd-read-injection-scanner.js +2 -2
  102. package/hooks/gsd-secret-read-guard.js +141 -6
  103. package/hooks/gsd-statusline.js +3 -2
  104. package/hooks/gsd-workflow-guard.js +2 -1
  105. package/hooks/lib/config-read.js +49 -0
  106. package/hooks/lib/git-cmd.js +3 -1
  107. package/hooks/lib/git-head-tree.js +474 -0
  108. package/hooks/lib/isolation-sentinel.js +97 -17
  109. package/hooks/lib/secret-read-exemptions.js +161 -0
  110. package/package.json +7 -2
  111. package/pi/package-agents/gsd-executor.md +1 -1
  112. package/pi/package-agents/gsd-plan-checker.md +1 -1
  113. package/pi/package-agents/gsd-planner.md +3 -3
  114. package/pi/package-agents/gsd-verifier.md +2 -2
  115. package/pi/package-agents/manifest.json +4 -4
  116. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  117. package/scripts/changeset/cli.cjs +2 -2
  118. package/scripts/changeset/serialize.cjs +12 -2
  119. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  120. package/scripts/ci-fix-next-rerun.cjs +149 -0
  121. package/scripts/ci-next-health.cjs +52 -9
  122. package/scripts/ci-test-scope.cjs +77 -71
  123. package/scripts/command-contract-helpers.cjs +56 -7
  124. package/scripts/docs-guard-registry.cjs +3 -0
  125. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  126. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  127. package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
  128. package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
  129. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  130. package/scripts/lint-removed-but-needed.cjs +562 -50
  131. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  132. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  133. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  134. package/skills/gsd-autonomous/SKILL.md +1 -1
  135. package/vscode/package.json +1 -1
@@ -25,6 +25,7 @@ const capabilityRegistry = require('./capability-registry.cjs');
25
25
  const { hostBehaviorsFor } = require('./runtime-name-policy.cjs');
26
26
  const { resolveConfigHomeFromDescriptor } = require('./runtime-homes.cjs');
27
27
  const { resolveActiveRuntime } = require('./runtime-slash.cjs');
28
+ const { readConfigFileBounded } = require('./config-file-read.cjs');
28
29
 
29
30
  /**
30
31
  * Runtimes whose agent config is static frontmatter/TOML baked at install time,
@@ -226,7 +227,9 @@ function _readRuntimeConfig(cwd, { fsStatSync = fs.statSync } = {}) {
226
227
  const candidate = path.join(probe, '.planning', 'config.json');
227
228
  try {
228
229
  fsStatSync(candidate);
229
- const raw = fs.readFileSync(candidate, 'utf8');
230
+ // Bounded read (#5759): a FIFO/device/oversized config throws and is walked past like a
231
+ // missing one; null (raced away after the stat) makes JSON.parse('') throw into the same catch.
232
+ const raw = readConfigFileBounded(candidate) ?? '';
230
233
  const parsed = JSON.parse(raw);
231
234
  if (parsed && typeof parsed === 'object') return parsed;
232
235
  return {};
@@ -31,6 +31,8 @@ exports.applyPreserveWhenUnchanged = applyPreserveWhenUnchanged;
31
31
  exports.applyStatePreservation = applyStatePreservation;
32
32
  exports.transitionCore = transitionCore;
33
33
  exports.sliceCurrentPositionSection = sliceCurrentPositionSection;
34
+ exports.readPlanPosition = readPlanPosition;
35
+ exports.readRecordedPlanTotal = readRecordedPlanTotal;
34
36
  exports.isStatePastPhase = isStatePastPhase;
35
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
36
38
  const frontmatter = require("./frontmatter.cjs");
@@ -1261,6 +1263,94 @@ function planNumberFrom(digits) {
1261
1263
  const n = Number(digits);
1262
1264
  return Number.isSafeInteger(n) ? n : null;
1263
1265
  }
1266
+ /**
1267
+ * The one plan-position parse: legacy pair first, then the hybrid, then
1268
+ * compound. `advancePlanCore` and `readPlanPosition` both consume it, so the
1269
+ * precedence is decided in exactly one place (#5786).
1270
+ *
1271
+ * These branches decide ONE thing: which numbers the position is read from.
1272
+ * They deliberately do not record which FIELD supplied them, because the
1273
+ * advance write path writes every spelling back from its own raw text (#3791
1274
+ * review round 6, B1/M1).
1275
+ *
1276
+ * Every branch reads its numbers out of an ANCHORED match's capture groups.
1277
+ * Nothing here calls parseInt on a raw field value, so a value the grammar
1278
+ * does not fully describe cannot half-parse into a plausible number.
1279
+ */
1280
+ function parsePlanFields(content) {
1281
+ const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
1282
+ const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1283
+ const planField = (0, state_document_cjs_1.stateExtractField)(content, 'Plan');
1284
+ const legacyNMatch = legacyPlan ? PLAN_SHAPE_N.exec(legacyPlan) : null;
1285
+ const legacyNofMMatch = legacyPlan ? PLAN_SHAPE_N_OF_M.exec(legacyPlan) : null;
1286
+ const totalNMatch = legacyTotal ? PLAN_SHAPE_N.exec(legacyTotal) : null;
1287
+ const planNofMMatch = planField ? PLAN_SHAPE_N_OF_M.exec(planField) : null;
1288
+ const planNMatch = planField ? PLAN_SHAPE_N.exec(planField) : null;
1289
+ let current = null;
1290
+ let total = null;
1291
+ if (legacyPlan && legacyTotal && (legacyNMatch || legacyNofMMatch) && totalNMatch) {
1292
+ // Legacy pair wins whenever both fields are present and both are readable,
1293
+ // even if the Current Plan value also carries an "of M" — the explicit
1294
+ // sibling field is the stated intent, so it supplies the total.
1295
+ current = planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1]);
1296
+ total = planNumberFrom(totalNMatch[1]);
1297
+ }
1298
+ else if (legacyNofMMatch) {
1299
+ // Hybrid: legacy field name, compound value, no readable Total Plans
1300
+ // sibling. Written by hand (and by agents) often enough to be worth
1301
+ // reading — #3784.
1302
+ current = planNumberFrom(legacyNofMMatch[1]);
1303
+ total = planNumberFrom(legacyNofMMatch[2]);
1304
+ }
1305
+ else if (planNofMMatch) {
1306
+ current = planNumberFrom(planNofMMatch[1]);
1307
+ total = planNumberFrom(planNofMMatch[2]);
1308
+ }
1309
+ // No branch for a bare `Plan: N` paired with a `Total Plans in Phase: M`
1310
+ // sibling and no `Current Plan` at all (#3791 review round 6, M2). A revision
1311
+ // of this PR accepted it; base did not (its `else if (planField)` arm had no
1312
+ // `of M` match and errored via NaN), and it is out of #3784's scope, which is
1313
+ // the hybrid `Current Plan: N of M`. It cannot be given the schema-row +
1314
+ // forcing-test coupling the other shapes have, either: `Plan` is body-only,
1315
+ // `buildStateFrontmatter` never reads it into frontmatter, so there is no
1316
+ // `current_*` key to hang a row on. An accepted shape with no schema row and
1317
+ // no forcing test is exactly the drift this diff is otherwise built to
1318
+ // prevent, so the shape is refused and named in the error instead.
1319
+ return { legacyPlan, planField, legacyNMatch, legacyNofMMatch, planNofMMatch, planNMatch, current, total };
1320
+ }
1321
+ /**
1322
+ * Read the recorded plan position out of a STATE.md document through the one
1323
+ * plan-position grammar, or `null` when no readable position is recorded
1324
+ * (`Plan: 1 of ?`, `Total Plans in Phase: TBD`, no field at all). #5786.
1325
+ */
1326
+ function readPlanPosition(content) {
1327
+ const { current, total } = parsePlanFields(content);
1328
+ return current === null || total === null ? null : { current, total };
1329
+ }
1330
+ /**
1331
+ * The plan total a STATE.md document records, for the `init --validate`
1332
+ * plan-count check: a readable `Total Plans in Phase` first, else the
1333
+ * of-M total read straight from the plan fields: `Current Plan`'s of-M is
1334
+ * tried first, then `Plan`'s, and convertibility is checked per field (an
1335
+ * unconvertible `Current Plan` M falls through to `Plan`'s M), unlike
1336
+ * `parsePlanFields`, which commits to the first matching field. The fallback
1337
+ * does not go through the legacy pair, so an unconvertible `Total Plans in
1338
+ * Phase` beside `Current Plan: N of M` still yields M. `null` when neither
1339
+ * yields a convertible M. #5786.
1340
+ */
1341
+ function readRecordedPlanTotal(content) {
1342
+ // An explicit `Total Plans in Phase` wins over an `of M` in either plan field.
1343
+ const raw = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1344
+ const match = raw ? PLAN_SHAPE_N.exec(raw) : null;
1345
+ const t = match ? planNumberFrom(match[1]) : null;
1346
+ if (t !== null)
1347
+ return t;
1348
+ const { legacyNofMMatch, planNofMMatch } = parsePlanFields(content);
1349
+ const fromCurrentPlan = legacyNofMMatch ? planNumberFrom(legacyNofMMatch[2]) : null;
1350
+ if (fromCurrentPlan !== null)
1351
+ return fromCurrentPlan;
1352
+ return planNofMMatch ? planNumberFrom(planNofMMatch[2]) : null;
1353
+ }
1264
1354
  /**
1265
1355
  * Advance the leading integer of a written plan value, preserving everything
1266
1356
  * the author wrote around it: the zero-padding width ("04" -> "05") and any
@@ -1483,47 +1573,7 @@ function advancePlanCore(content, deps) {
1483
1573
  // `planSourceField`/`planRawValue` pair here and then wrote the OTHER
1484
1574
  // spelling from this one's numbers, which is precisely how a field ended up
1485
1575
  // holding a value nothing had derived for it.
1486
- const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
1487
- const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1488
- const planField = (0, state_document_cjs_1.stateExtractField)(content, 'Plan');
1489
- // Every branch below reads its numbers out of an ANCHORED match's capture
1490
- // groups. Nothing here calls parseInt on a raw field value, so a value the
1491
- // grammar does not fully describe cannot half-parse into a plausible number.
1492
- const legacyNMatch = legacyPlan ? PLAN_SHAPE_N.exec(legacyPlan) : null;
1493
- const legacyNofMMatch = legacyPlan ? PLAN_SHAPE_N_OF_M.exec(legacyPlan) : null;
1494
- const totalNMatch = legacyTotal ? PLAN_SHAPE_N.exec(legacyTotal) : null;
1495
- const planNofMMatch = planField ? PLAN_SHAPE_N_OF_M.exec(planField) : null;
1496
- const planNMatch = planField ? PLAN_SHAPE_N.exec(planField) : null;
1497
- let parsedCurrent = null;
1498
- let parsedTotal = null;
1499
- if (legacyPlan && legacyTotal && (legacyNMatch || legacyNofMMatch) && totalNMatch) {
1500
- // Legacy pair wins whenever both fields are present and both are readable,
1501
- // even if the Current Plan value also carries an "of M" — the explicit
1502
- // sibling field is the stated intent, so it supplies the total.
1503
- parsedCurrent = planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1]);
1504
- parsedTotal = planNumberFrom(totalNMatch[1]);
1505
- }
1506
- else if (legacyNofMMatch) {
1507
- // Hybrid: legacy field name, compound value, no readable Total Plans
1508
- // sibling. Written by hand (and by agents) often enough to be worth
1509
- // reading — #3784.
1510
- parsedCurrent = planNumberFrom(legacyNofMMatch[1]);
1511
- parsedTotal = planNumberFrom(legacyNofMMatch[2]);
1512
- }
1513
- else if (planNofMMatch) {
1514
- parsedCurrent = planNumberFrom(planNofMMatch[1]);
1515
- parsedTotal = planNumberFrom(planNofMMatch[2]);
1516
- }
1517
- // No branch for a bare `Plan: N` paired with a `Total Plans in Phase: M`
1518
- // sibling and no `Current Plan` at all (#3791 review round 6, M2). A revision
1519
- // of this PR accepted it; base did not (its `else if (planField)` arm had no
1520
- // `of M` match and errored via NaN), and it is out of #3784's scope, which is
1521
- // the hybrid `Current Plan: N of M`. It cannot be given the schema-row +
1522
- // forcing-test coupling the other shapes have, either: `Plan` is body-only,
1523
- // `buildStateFrontmatter` never reads it into frontmatter, so there is no
1524
- // `current_*` key to hang a row on. An accepted shape with no schema row and
1525
- // no forcing test is exactly the drift this diff is otherwise built to
1526
- // prevent, so the shape is refused and named in the error instead.
1576
+ const { legacyPlan, planField, legacyNMatch, legacyNofMMatch, planNofMMatch, planNMatch, current: parsedCurrent, total: parsedTotal, } = parsePlanFields(content);
1527
1577
  if (parsedCurrent === null || parsedTotal === null) {
1528
1578
  return { content: reassemble(body), updated: [], data: { error: true } };
1529
1579
  }