@opengsd/gsd-core 1.3.0 → 1.4.0-rc.1

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 (98) hide show
  1. package/agents/gsd-advisor-researcher.md +1 -20
  2. package/agents/gsd-ai-researcher.md +1 -20
  3. package/agents/gsd-domain-researcher.md +1 -20
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-phase-researcher.md +92 -166
  6. package/agents/gsd-planner.md +9 -36
  7. package/agents/gsd-project-researcher.md +62 -141
  8. package/agents/gsd-ui-researcher.md +2 -21
  9. package/agents/gsd-verifier.md +8 -2
  10. package/bin/install.js +85 -4
  11. package/commands/gsd/graphify.md +11 -6
  12. package/commands/gsd/import.md +6 -2
  13. package/commands/gsd/plan-phase.md +2 -2
  14. package/gsd-core/bin/check-latest-version.cjs +3 -2
  15. package/gsd-core/bin/gsd-tools.cjs +238 -32
  16. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  17. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  18. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  19. package/gsd-core/bin/lib/commands.cjs +5 -4
  20. package/gsd-core/bin/lib/config.cjs +28 -4
  21. package/gsd-core/bin/lib/core.cjs +72 -28
  22. package/gsd-core/bin/lib/graphify.cjs +2 -2
  23. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  24. package/gsd-core/bin/lib/init.cjs +19 -3
  25. package/gsd-core/bin/lib/installer-migrations.cjs +61 -22
  26. package/gsd-core/bin/lib/intel.cjs +3 -20
  27. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  28. package/gsd-core/bin/lib/phase.cjs +3 -3
  29. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  30. package/gsd-core/bin/lib/research-store.cjs +167 -0
  31. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  32. package/gsd-core/bin/lib/security.cjs +73 -0
  33. package/gsd-core/bin/lib/shell-command-projection.cjs +3 -0
  34. package/gsd-core/bin/lib/validate.cjs +2 -2
  35. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  36. package/gsd-core/bin/lib/verification.cjs +193 -0
  37. package/gsd-core/bin/lib/verify.cjs +2 -2
  38. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  39. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  40. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  41. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  42. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  43. package/gsd-core/references/planner-load-graph-context.md +36 -0
  44. package/gsd-core/references/planning-config.md +3 -1
  45. package/gsd-core/references/research-documentation-lookup.md +29 -0
  46. package/gsd-core/references/research-philosophy.md +29 -0
  47. package/gsd-core/references/research-verification-protocol.md +27 -0
  48. package/gsd-core/workflows/execute-phase.md +19 -8
  49. package/gsd-core/workflows/help/modes/full.md +2 -2
  50. package/gsd-core/workflows/ingest-docs.md +3 -2
  51. package/gsd-core/workflows/plan-phase.md +14 -10
  52. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  53. package/gsd-core/workflows/review.md +22 -5
  54. package/gsd-core/workflows/ship.md +5 -8
  55. package/gsd-core/workflows/spec-phase.md +2 -1
  56. package/gsd-core/workflows/update.md +2 -1
  57. package/hooks/dist/gsd-context-monitor.js +1 -1
  58. package/hooks/dist/gsd-workflow-guard.js +1 -0
  59. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  60. package/hooks/gsd-context-monitor.js +1 -1
  61. package/hooks/gsd-workflow-guard.js +1 -0
  62. package/hooks/gsd-worktree-path-guard.js +1 -1
  63. package/package.json +4 -1
  64. package/scripts/affected-tests-lib.cjs +3 -2
  65. package/scripts/changeset/cli.cjs +183 -28
  66. package/scripts/changeset/lint.cjs +5 -4
  67. package/scripts/changeset/new.cjs +4 -4
  68. package/scripts/check-alias-drift.cjs +77 -71
  69. package/scripts/check-env.cjs +185 -179
  70. package/scripts/check-npm-integrity.cjs +115 -109
  71. package/scripts/ci-guard-runner.cjs +11 -5
  72. package/scripts/ci-prepare-test-scope.cjs +27 -22
  73. package/scripts/ci-rebase-check.cjs +46 -45
  74. package/scripts/ci-test-scope.cjs +6 -4
  75. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  76. package/scripts/gen-inventory-manifest.cjs +38 -32
  77. package/scripts/gen-research-agents.cjs +276 -0
  78. package/scripts/lib/cli-exit.cjs +56 -0
  79. package/scripts/lint-command-contract.cjs +28 -22
  80. package/scripts/lint-descriptions.cjs +32 -28
  81. package/scripts/lint-docs-required.cjs +4 -4
  82. package/scripts/lint-legacy-dir-name.cjs +56 -52
  83. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  84. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  85. package/scripts/lint-skill-deps.cjs +31 -26
  86. package/scripts/lint-test-file-count.allowlist.json +1 -0
  87. package/scripts/lint-test-file-count.cjs +5 -4
  88. package/scripts/mutation-matrix.cjs +6 -3
  89. package/scripts/prompt-injection-scan.sh +1 -1
  90. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  91. package/scripts/release-tarball-smoke.cjs +6 -4
  92. package/scripts/research-profiles.cjs +149 -0
  93. package/scripts/run-affected-tests.cjs +2 -1
  94. package/scripts/run-cross-platform-tests.cjs +11 -7
  95. package/scripts/run-tests.cjs +8 -7
  96. package/scripts/strip-prose-atrefs.cjs +1 -1
  97. package/scripts/sync-runtime-launcher.cjs +0 -3
  98. package/scripts/verify-npm-publish.cjs +14 -26
@@ -506,6 +506,7 @@ function loadConfig(cwd, options = {}) {
506
506
  effort: (parsed['effort']) || null,
507
507
  fast_mode: (parsed['fast_mode']) || null,
508
508
  agent_skills: (parsed['agent_skills']) || {},
509
+ agent_skills_security: (parsed['agent_skills_security']) || null,
509
510
  manager: (parsed['manager']) || {},
510
511
  response_language: get('response_language') || null,
511
512
  claude_md_path: get('claude_md_path') || null,
@@ -994,35 +995,38 @@ function extractCurrentMilestone(content, cwd) {
994
995
  const firstMatch = allMatches[0];
995
996
  const selected = allMatches.find((m) => !isClosed(m[1])) || firstMatch;
996
997
  const sectionStart = selected.index;
997
- const sectionMatch = selected;
998
- const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
999
- const restContent = content.slice(sectionStart + sectionMatch[0].length);
1000
- const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
1001
- let sectionEnd = content.length;
1002
- let fenceChar = null;
1003
- let fenceLen = 0;
1004
- let charOffset = 0;
1005
- for (const line of restContent.split('\n')) {
1006
- const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
1007
- if (fenceMatch) {
1008
- const char = fenceMatch[1][0];
1009
- const len = fenceMatch[1].length;
1010
- const trailing = fenceMatch[2] || '';
1011
- if (!fenceChar) {
1012
- fenceChar = char;
1013
- fenceLen = len;
998
+ const computeSectionEnd = (headingText, headingStart) => {
999
+ const level = (headingText.match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
1000
+ const rest = content.slice(headingStart + headingText.length);
1001
+ const stopPattern = new RegExp(`^#{1,${level}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
1002
+ let end = content.length;
1003
+ let fc = null;
1004
+ let fl = 0;
1005
+ let off = 0;
1006
+ for (const line of rest.split('\n')) {
1007
+ const fm = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
1008
+ if (fm) {
1009
+ const ch = fm[1][0];
1010
+ const ln = fm[1].length;
1011
+ const trailing = fm[2] || '';
1012
+ if (!fc) {
1013
+ fc = ch;
1014
+ fl = ln;
1015
+ }
1016
+ else if (ch === fc && ln >= fl && /^\s*$/.test(trailing)) {
1017
+ fc = null;
1018
+ fl = 0;
1019
+ }
1014
1020
  }
1015
- else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
1016
- fenceChar = null;
1017
- fenceLen = 0;
1021
+ else if (!fc && stopPattern.test(line)) {
1022
+ end = headingStart + headingText.length + off;
1023
+ break;
1018
1024
  }
1025
+ off += line.length + 1;
1019
1026
  }
1020
- else if (!fenceChar && nextMilestonePattern.test(line)) {
1021
- sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
1022
- break;
1023
- }
1024
- charOffset += line.length + 1;
1025
- }
1027
+ return end;
1028
+ };
1029
+ const sectionEnd = computeSectionEnd(selected[0], sectionStart);
1026
1030
  const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧)/im;
1027
1031
  const firstMilestoneMatch = content.match(anyMilestonePattern);
1028
1032
  const preambleCutoff = firstMilestoneMatch
@@ -1030,11 +1034,36 @@ function extractCurrentMilestone(content, cwd) {
1030
1034
  : firstMatch.index;
1031
1035
  const beforeMilestones = content.slice(0, preambleCutoff);
1032
1036
  const currentSection = content.slice(sectionStart, sectionEnd);
1037
+ // Multi-milestone roadmaps split each added milestone across two version-bearing
1038
+ // headings: a `## Phases` checklist subsection (early) and a dedicated
1039
+ // `## Milestone … (Phase Details)` section (late) holding the `### Phase N:`
1040
+ // detail headers. The scope window above stops at the next version-bearing
1041
+ // heading — the current milestone's OWN Phase Details heading — leaving those
1042
+ // detail headers outside `currentSection`. Append that section so phase
1043
+ // resolution and counting see the current milestone's phases. Anchor the lookup
1044
+ // to the SELECTED heading's specific version token (boundary-aware, so a
1045
+ // `v3.0` state does not match a `v3.0-A` sub-milestone) so sibling milestones
1046
+ // that share a version prefix do not cross-pollinate. (#730)
1047
+ const selectedVersionToken = selected[1].match(/v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i)?.[0];
1048
+ const detailsVersionBoundary = selectedVersionToken
1049
+ ? new RegExp(`${escapeRegex(selectedVersionToken)}(?![\\w.-])`, 'i')
1050
+ : null;
1051
+ let detailsSection = '';
1052
+ const detailsMatch = allMatches.find((m) => /\(Phase\s+Details\)/i.test(m[1]) &&
1053
+ !isClosed(m[1]) &&
1054
+ (!detailsVersionBoundary || detailsVersionBoundary.test(m[1])) &&
1055
+ (m.index ?? 0) >= sectionEnd);
1056
+ if (detailsMatch) {
1057
+ const detailsStart = detailsMatch.index ?? 0;
1058
+ detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
1059
+ }
1033
1060
  const preamble = beforeMilestones
1034
1061
  .replace(/<details>[\s\S]*?<\/details>/gi, '')
1035
1062
  .replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
1036
1063
  .replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
1037
- return preamble + currentSection;
1064
+ return detailsSection
1065
+ ? preamble + currentSection + '\n' + detailsSection
1066
+ : preamble + currentSection;
1038
1067
  }
1039
1068
  /**
1040
1069
  * Replace a pattern only in the current milestone section of ROADMAP.md.
@@ -1399,7 +1428,12 @@ const VALID_GRANULARITIES = new Set(['coarse', 'standard', 'fine']);
1399
1428
  /**
1400
1429
  * Resolve the planning granularity for a phase type (#68).
1401
1430
  */
1402
- function resolveGranularityInternal(cwd, phaseType) {
1431
+ function resolveGranularityInternal(cwd, phaseType, override) {
1432
+ if (override !== undefined && override !== null && override !== '') {
1433
+ if (VALID_GRANULARITIES.has(override)) {
1434
+ return override;
1435
+ }
1436
+ }
1403
1437
  const config = loadConfig(cwd);
1404
1438
  const configGranularities = config['granularities'];
1405
1439
  const perPhase = (phaseType && configGranularities && typeof configGranularities === 'object')
@@ -1418,6 +1452,15 @@ function resolveGranularityInternal(cwd, phaseType) {
1418
1452
  }
1419
1453
  return 'standard';
1420
1454
  }
1455
+ /**
1456
+ * Validate a CLI granularity override at the command boundary. Empty/null/undefined
1457
+ * are treated as "no override" (no-op). An invalid non-empty value calls `fail`.
1458
+ */
1459
+ function assertValidGranularityOverride(override, fail) {
1460
+ if (override !== undefined && override !== null && override !== '' && !VALID_GRANULARITIES.has(override)) {
1461
+ fail(`invalid granularity '${override}' (valid: ${[...VALID_GRANULARITIES].join(', ')})`);
1462
+ }
1463
+ }
1421
1464
  /**
1422
1465
  * #3024 — Resolve a model for a specific dynamic-routing attempt.
1423
1466
  */
@@ -1956,6 +1999,7 @@ module.exports = {
1956
1999
  resolveModelForTier,
1957
2000
  resolveGranularityInternal,
1958
2001
  VALID_GRANULARITIES,
2002
+ assertValidGranularityOverride,
1959
2003
  resolveEffortInternal,
1960
2004
  resolveFastModeInternal,
1961
2005
  resolveEffortForTier,
@@ -35,7 +35,7 @@ function isGraphifyEnabled(planningDir) {
35
35
  return true;
36
36
  return false;
37
37
  }
38
- catch (_e) {
38
+ catch {
39
39
  return false;
40
40
  }
41
41
  }
@@ -157,7 +157,7 @@ function safeReadJson(filePath) {
157
157
  return null;
158
158
  return JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
159
159
  }
160
- catch (_e) {
160
+ catch {
161
161
  return null;
162
162
  }
163
163
  }
@@ -34,8 +34,8 @@ function routeInitCommand({ init, args, cwd, raw, error }) {
34
34
  init.cmdInitExecutePhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'] });
35
35
  },
36
36
  'plan-phase': () => {
37
- const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['validate', 'tdd']);
38
- init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'] });
37
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['granularity'], ['validate', 'tdd']);
38
+ init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'], granularity: namedArgs['granularity'] });
39
39
  },
40
40
  'new-project': () => init.cmdInitNewProject(cwd, raw),
41
41
  'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
@@ -28,7 +28,7 @@ const security_cjs_1 = require("./security.cjs");
28
28
  const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
29
29
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
30
30
  const frontmatterMod = require("./frontmatter.cjs");
31
- const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, gitWorktreeInfoInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error, checkAgentsInstalled, phaseTokenMatches, } = core;
31
+ const { loadConfig, resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, gitWorktreeInfoInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error, checkAgentsInstalled, phaseTokenMatches, } = core;
32
32
  const { planningPaths, planningDir, planningRoot, findContextMdIn, } = planningWorkspace;
33
33
  const { determinePhaseStatus } = commandsMod;
34
34
  const { extractFrontmatter } = frontmatterMod;
@@ -297,11 +297,15 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
297
297
  expectedPhaseDirPlan = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningPaths(cwd).phases, dirName)));
298
298
  }
299
299
  }
300
+ const granularityOverride = options['granularity'];
301
+ assertValidGranularityOverride(granularityOverride, error);
302
+ const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
300
303
  const result = {
301
304
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
302
305
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
303
306
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
304
307
  tdd_mode: options['tdd'] || config.tdd_mode || false,
308
+ granularity,
305
309
  research_enabled: config.research,
306
310
  plan_checker_enabled: config.plan_checker,
307
311
  nyquist_validation_enabled: config.nyquist_validation,
@@ -1521,6 +1525,11 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1521
1525
  skillPaths = [skillPaths];
1522
1526
  if (!Array.isArray(skillPaths) || skillPaths.length === 0)
1523
1527
  return '';
1528
+ // Hoist trusted roots computation before the loop: loadTrustedGlobalRoots does
1529
+ // realpathSync I/O and should run at most once per call, not once per failing skill.
1530
+ // It returns [] cheaply when no roots are configured, so the realpath cost only
1531
+ // occurs when the caller has actually set trusted_global_roots.
1532
+ const trustedGlobalRoots = (0, security_cjs_1.loadTrustedGlobalRoots)(config);
1524
1533
  const validPaths = [];
1525
1534
  for (const skillPath of skillPaths) {
1526
1535
  if (typeof skillPath !== 'string')
@@ -1548,8 +1557,15 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1548
1557
  }
1549
1558
  const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
1550
1559
  if (!pathCheck['safe']) {
1551
- process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
1552
- continue;
1560
+ const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => {
1561
+ const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true });
1562
+ return Boolean(rootCheck['safe']);
1563
+ });
1564
+ if (!acceptedViaTrustedRoot) {
1565
+ process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
1566
+ continue;
1567
+ }
1568
+ process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
1553
1569
  }
1554
1570
  validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
1555
1571
  continue;
@@ -175,16 +175,46 @@ function migrationChecksum(migration) {
175
175
  };
176
176
  return `sha256:${sha256Text(JSON.stringify(serializable))}`;
177
177
  }
178
- function assertAppliedMigrationChecksums(applied, migrations) {
178
+ // Rewrite the stored checksum of any already-applied entry whose id drifted, so the
179
+ // drift is reconciled durably and not re-detected on every subsequent run (issue #670).
180
+ // Returns the number of entries actually changed (so callers know whether a write is needed).
181
+ function reconcileDriftedChecksums(appliedEntries, checksumDrift) {
182
+ if (!Array.isArray(checksumDrift) || checksumDrift.length === 0)
183
+ return 0;
184
+ const reconcile = new Map(checksumDrift.map((d) => [d.id, d.currentChecksum]));
185
+ let changed = 0;
186
+ for (let i = 0; i < appliedEntries.length; i++) {
187
+ const existing = appliedEntries[i];
188
+ if (existing && typeof existing.id === 'string' && reconcile.has(existing.id)) {
189
+ const next = reconcile.get(existing.id);
190
+ if (existing.checksum !== next) {
191
+ appliedEntries[i] = { ...existing, checksum: next };
192
+ changed += 1;
193
+ }
194
+ }
195
+ }
196
+ return changed;
197
+ }
198
+ function collectAppliedChecksumDrift(applied, migrations) {
199
+ const drift = [];
179
200
  for (const migration of migrations) {
180
201
  const entry = applied.get(migration.id);
181
202
  if (!entry || !entry.checksum)
182
203
  continue;
183
- const checksum = migrationChecksum(migration);
184
- if (entry.checksum !== checksum) {
185
- throw new Error(`applied migration checksum changed for ${migration.id}; create a new fix-forward migration id`);
204
+ const currentChecksum = migrationChecksum(migration);
205
+ if (entry.checksum !== currentChecksum) {
206
+ // An already-applied migration is never re-run (it is filtered out of `pending`),
207
+ // so a checksum drift here is functionally inert. A prior release may have edited a
208
+ // shipped migration body (see issue #670). Surface it for reconciliation instead of
209
+ // hard-aborting the user's upgrade.
210
+ drift.push({
211
+ id: migration.id,
212
+ storedChecksum: entry.checksum,
213
+ currentChecksum,
214
+ });
186
215
  }
187
216
  }
217
+ return drift;
188
218
  }
189
219
  function migrationMatchesContext(migration, { runtime, scope }) {
190
220
  if (Array.isArray(migration.runtimes) && migration.runtimes.length > 0) {
@@ -387,7 +417,7 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
387
417
  const validatedMigrations = migrations.map((migration) => (0, installer_migration_authoring_cjs_1.validateInstallerMigrationRecord)(migration));
388
418
  const scopedMigrations = validatedMigrations.filter((migration) => migrationMatchesContext(migration, { runtime, scope }));
389
419
  const applied = appliedMigrationEntries(state);
390
- assertAppliedMigrationChecksums(applied, scopedMigrations);
420
+ const checksumDrift = collectAppliedChecksumDrift(applied, scopedMigrations);
391
421
  const pending = scopedMigrations.filter((migration) => !applied.has(migration.id));
392
422
  const actions = [];
393
423
  const blocked = [];
@@ -474,6 +504,7 @@ function planInstallerMigrations({ configDir, runtime = null, scope = null, migr
474
504
  pendingMigrations: pending,
475
505
  actions,
476
506
  blocked,
507
+ checksumDrift,
477
508
  };
478
509
  }
479
510
  function uniqueActionMigrationIds(actions) {
@@ -630,6 +661,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
630
661
  const state = readInstallState(configDir);
631
662
  const applied = appliedMigrationIds(state);
632
663
  const nextApplied = [...state.appliedMigrations];
664
+ reconcileDriftedChecksums(nextApplied, plan.checksumDrift);
633
665
  const actionsByMigrationId = new Map();
634
666
  for (const action of plan.actions) {
635
667
  if (action.migrationId && !actionsByMigrationId.has(action.migrationId)) {
@@ -684,30 +716,36 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
684
716
  }
685
717
  }
686
718
  function markPendingMigrationsApplied({ configDir, plan, now = () => new Date().toISOString(), }) {
687
- if (!plan || !Array.isArray(plan.pendingMigrationIds) || plan.pendingMigrationIds.length === 0) {
719
+ if (!plan)
720
+ return [];
721
+ const hasPending = Array.isArray(plan.pendingMigrationIds) && plan.pendingMigrationIds.length > 0;
722
+ const hasDrift = Array.isArray(plan.checksumDrift) && plan.checksumDrift.length > 0;
723
+ if (!hasPending && !hasDrift)
688
724
  return [];
689
- }
690
725
  const appliedAt = now();
691
726
  const state = readInstallState(configDir);
692
727
  const applied = appliedMigrationIds(state);
693
- const checksumsByMigrationId = new Map();
694
- for (const migration of plan.pendingMigrations || []) {
695
- checksumsByMigrationId.set(migration.id, migrationChecksum(migration));
696
- }
697
728
  const nextApplied = [...state.appliedMigrations];
729
+ const reconciledCount = reconcileDriftedChecksums(nextApplied, plan.checksumDrift);
698
730
  const newlyApplied = [];
699
- for (const id of plan.pendingMigrationIds) {
700
- if (applied.has(id))
701
- continue;
702
- nextApplied.push({
703
- id,
704
- appliedAt,
705
- journal: null,
706
- checksum: checksumsByMigrationId.get(id) || null,
707
- });
708
- newlyApplied.push(id);
731
+ if (hasPending) {
732
+ const checksumsByMigrationId = new Map();
733
+ for (const migration of plan.pendingMigrations || []) {
734
+ checksumsByMigrationId.set(migration.id, migrationChecksum(migration));
735
+ }
736
+ for (const id of plan.pendingMigrationIds) {
737
+ if (applied.has(id))
738
+ continue;
739
+ nextApplied.push({
740
+ id,
741
+ appliedAt,
742
+ journal: null,
743
+ checksum: checksumsByMigrationId.get(id) || null,
744
+ });
745
+ newlyApplied.push(id);
746
+ }
709
747
  }
710
- if (newlyApplied.length > 0) {
748
+ if (newlyApplied.length > 0 || reconciledCount > 0) {
711
749
  writeInstallState(configDir, {
712
750
  schemaVersion: 1,
713
751
  appliedMigrations: nextApplied,
@@ -776,6 +814,7 @@ module.exports = {
776
814
  applyInstallerMigrationPlan,
777
815
  classifyArtifact,
778
816
  discoverInstallerMigrations,
817
+ migrationChecksum,
779
818
  planInstallerMigrations,
780
819
  readInstallManifest,
781
820
  readInstallState,
@@ -59,7 +59,7 @@ function isIntelEnabled(planningDir) {
59
59
  return true;
60
60
  return false;
61
61
  }
62
- catch (_e) {
62
+ catch {
63
63
  return false;
64
64
  }
65
65
  }
@@ -86,7 +86,7 @@ function safeReadJson(filePath) {
86
86
  return null;
87
87
  return JSON.parse(raw);
88
88
  }
89
- catch (_e) {
89
+ catch {
90
90
  return null;
91
91
  }
92
92
  }
@@ -101,7 +101,7 @@ function hashFile(filePath) {
101
101
  return null;
102
102
  return node_crypto_1.default.createHash('sha256').update(content).digest('hex');
103
103
  }
104
- catch (_e) {
104
+ catch {
105
105
  return null;
106
106
  }
107
107
  }
@@ -147,23 +147,6 @@ function matchesInValue(value, lowerTerm) {
147
147
  }
148
148
  return false;
149
149
  }
150
- /**
151
- * Search for a term in arch.md text content.
152
- * Returns matching lines.
153
- */
154
- function searchArchMd(filePath, term) {
155
- try {
156
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
157
- if (content === null)
158
- return [];
159
- const lowerTerm = term.toLowerCase();
160
- const lines = content.split(/\r?\n/);
161
- return lines.filter(line => line.toLowerCase().includes(lowerTerm));
162
- }
163
- catch (_e) {
164
- return [];
165
- }
166
- }
167
150
  /**
168
151
  * Query intel files for a search term.
169
152
  * Searches across all JSON intel files (keys and values) and arch.md (text lines).