@opengsd/gsd-core 1.18.0 → 1.19.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +2 -2
- package/agents/gsd-code-fixer.md +4 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/gsd-core/bin/gsd-tools.cjs +106 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +156 -74
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +17 -0
- package/gsd-core/bin/lib/config-loader.cjs +10 -0
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +34 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +65 -1
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +43 -1
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-id.cjs +21 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
- package/gsd-core/bin/lib/phase.cjs +115 -26
- package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -25
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +121 -16
- package/gsd-core/bin/lib/smart-entry.cjs +13 -0
- package/gsd-core/bin/lib/state-contract.cjs +29 -9
- package/gsd-core/bin/lib/state-document.cjs +12 -4
- package/gsd-core/bin/lib/state-transition.cjs +119 -24
- package/gsd-core/bin/lib/state.cjs +50 -28
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +2 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review.md +61 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +9 -5
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase.md +13 -13
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/review.md +16 -13
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +3 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +35 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -1
- package/hooks/dist/gsd-worktree-path-guard.js +28 -1
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +35 -2
- package/hooks/gsd-windsurf-pre-write.js +28 -1
- package/hooks/gsd-workflow-guard.js +1 -1
- package/hooks/gsd-worktree-path-guard.js +28 -1
- package/hooks/hooks.json +1 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -6
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +769 -0
- package/pi/package-agents/gsd-code-reviewer.md +402 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +894 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +51 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +20 -7
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -233,14 +233,17 @@ function _resolveCommandsGsdDir() {
|
|
|
233
233
|
* Only directories whose names start with gsd- are included so user-created
|
|
234
234
|
* skills (without the gsd- prefix) are not accidentally pulled in.
|
|
235
235
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
236
|
+
* Dependencies use the same parseRequires, parseCallsAgents, and
|
|
237
|
+
* workflowAgentRefs helpers as loadSkillsManifest. Workflow references resolve
|
|
238
|
+
* against this runtime's own gsd-core/workflows tree, including its fragments,
|
|
239
|
+
* so a custom config directory never borrows another installation's agents.
|
|
238
240
|
*
|
|
239
241
|
* Returns an empty Map when the skills dir does not exist.
|
|
240
242
|
*/
|
|
241
243
|
function _loadInstalledSkillsManifest(configDir) {
|
|
242
244
|
const manifest = new Map();
|
|
243
245
|
const skillsDir = node_path_1.default.join(configDir, 'skills');
|
|
246
|
+
const workflowsDir = node_path_1.default.join(configDir, 'gsd-core', 'workflows');
|
|
244
247
|
if (!node_fs_1.default.existsSync(skillsDir))
|
|
245
248
|
return manifest;
|
|
246
249
|
let entries;
|
|
@@ -282,12 +285,15 @@ function _loadInstalledSkillsManifest(configDir) {
|
|
|
282
285
|
// installed-runtime dependency resolution can never silently diverge from
|
|
283
286
|
// the source-tree path (single source of truth — no duplicated regex).
|
|
284
287
|
manifest.set(stem, content ? parseRequires(content) : []);
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
// Installed skills delegate to workflows just as source commands do.
|
|
289
|
+
// Preserve their agent closure so surface materialization only prunes
|
|
290
|
+
// agents no retained skill requires (#5356).
|
|
291
|
+
manifest.set(`_calls_agents_${stem}`, [
|
|
292
|
+
...new Set([
|
|
293
|
+
...parseCallsAgents(content),
|
|
294
|
+
...workflowAgentRefs(content, workflowsDir),
|
|
295
|
+
]),
|
|
296
|
+
]);
|
|
291
297
|
}
|
|
292
298
|
return manifest;
|
|
293
299
|
}
|
|
@@ -856,6 +856,7 @@ const VALID_CONVERTER_NAMES = new Set([
|
|
|
856
856
|
'convertClaudeCommandToKimiSkill',
|
|
857
857
|
'convertClaudeCommandToKimiCodeSkill',
|
|
858
858
|
'convertClaudeCommandToOpencodeSkill',
|
|
859
|
+
'convertClaudeCommandToPiSkill',
|
|
859
860
|
'convertClaudeCommandToTraeSkill',
|
|
860
861
|
'convertClaudeCommandToWindsurfSkill',
|
|
861
862
|
'convertClaudeCommandToWindsurfWorkflow',
|
|
@@ -879,6 +880,8 @@ const VALID_CONVERTER_NAMES = new Set([
|
|
|
879
880
|
// and the kilo/opencode agent converters (shared with those runtimes'
|
|
880
881
|
// commands-kind entries, options-bag signature `(content, {isAgent, modelOverride})`).
|
|
881
882
|
'convertClaudeAgentToHermesAgent',
|
|
883
|
+
// #5569 (ADR-5559 §3) — the role file Pi's gsd_dispatch loads.
|
|
884
|
+
'convertClaudeAgentToPiAgent',
|
|
882
885
|
'convertClaudeToKiloFrontmatter',
|
|
883
886
|
'convertClaudeToOpencodeFrontmatter',
|
|
884
887
|
]);
|
|
@@ -1236,6 +1239,20 @@ function validateArtifactKindEntry(capId, entry, prefix) {
|
|
|
1236
1239
|
}
|
|
1237
1240
|
}
|
|
1238
1241
|
|
|
1242
|
+
// excludeSuffixes — optional; filename suffixes the agents staging skips (#5569: Pi omits .compact.md)
|
|
1243
|
+
if (entry.excludeSuffixes !== undefined) {
|
|
1244
|
+
if (!Array.isArray(entry.excludeSuffixes)) {
|
|
1245
|
+
errors.push(ctx + '.excludeSuffixes must be an array of non-empty strings if present');
|
|
1246
|
+
} else {
|
|
1247
|
+
for (let i = 0; i < entry.excludeSuffixes.length; i++) {
|
|
1248
|
+
const s = entry.excludeSuffixes[i];
|
|
1249
|
+
if (typeof s !== 'string' || s.length === 0 || s.includes('/') || s.includes('\\')) {
|
|
1250
|
+
errors.push(ctx + '.excludeSuffixes[' + i + '] must be a non-empty string without path separators');
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1239
1256
|
// converter — required; must be a string or null (closed ConverterName enum — now enforced in phase 5e)
|
|
1240
1257
|
if (!Object.prototype.hasOwnProperty.call(entry, 'converter')) {
|
|
1241
1258
|
errors.push(ctx + '.converter is required (must be a string or null)');
|
|
@@ -156,6 +156,15 @@ const CONFIG_DEFAULTS = {
|
|
|
156
156
|
planner_stall_detection_enabled: _getNestedConfigDefault('planner', 'stall_detection_enabled'),
|
|
157
157
|
max_prompt_tokens: _getNestedConfigDefault('review', 'max_prompt_tokens'),
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Resolve `git.repo_tagging` (#5706) from hand-edited configuration. Only the
|
|
161
|
+
* two documented enum members are honored; any other stored value (a number,
|
|
162
|
+
* null, an unknown string, a wrong-case spelling) reads as the default `single`
|
|
163
|
+
* and never throws, so a bad hand edit cannot enable per-repository tagging.
|
|
164
|
+
*/
|
|
165
|
+
function resolveRepoTagging(value) {
|
|
166
|
+
return value === 'per_repo' ? 'per_repo' : 'single';
|
|
167
|
+
}
|
|
159
168
|
/**
|
|
160
169
|
* Resolve the planner watchdog policy from hand-edited configuration.
|
|
161
170
|
* Only a real JSON boolean may override the default; strings/numbers/null
|
|
@@ -849,6 +858,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
|
|
|
849
858
|
base_branch: get('base_branch', { section: 'git', field: 'base_branch' }),
|
|
850
859
|
protected_branches: getNested('git', 'protected_branches'),
|
|
851
860
|
allow_default_branch_commits: getNested('git', 'allow_default_branch_commits'),
|
|
861
|
+
repo_tagging: resolveRepoTagging(getNested('git', 'repo_tagging')),
|
|
852
862
|
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
|
853
863
|
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
|
854
864
|
quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
|
|
@@ -30,7 +30,7 @@ const modelProfiles = require("./model-profiles.cjs");
|
|
|
30
30
|
const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
const configSchema = require("./config-schema.cjs");
|
|
33
|
-
const { VALID_CONFIG_KEYS, isValidConfigKey, getCapabilityConfigSchema } = configSchema;
|
|
33
|
+
const { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, isValidConfigKey, getCapabilityConfigSchema } = configSchema;
|
|
34
34
|
const secrets_cjs_1 = require("./secrets.cjs");
|
|
35
35
|
const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
|
|
36
36
|
const configuration_cjs_1 = require("./configuration.cjs");
|
|
@@ -66,6 +66,8 @@ const SHIP_PR_BODY_TEMPLATE_TOKENS = new Set([
|
|
|
66
66
|
'padded_phase',
|
|
67
67
|
]);
|
|
68
68
|
const SHIP_PR_BODY_SOURCE_RE = /^(ROADMAP|PLAN|SUMMARY|VERIFICATION|STATE|REQUIREMENTS|CONTEXT)\.md\s+##\s+[^\r\n#][^\r\n]*$/;
|
|
69
|
+
/** Allowed values of `git.repo_tagging` (#5706): `single` is the default. */
|
|
70
|
+
const VALID_REPO_TAGGING_MODES = Object.freeze(['single', 'per_repo']);
|
|
69
71
|
// ADR-612 PR-5: configuration accepts every convention the runtime can read.
|
|
70
72
|
// Keep this distinct from roadmap-upgrade's supported target set: sequential
|
|
71
73
|
// is valid project configuration but is not a migration destination.
|
|
@@ -87,6 +89,9 @@ const SCHEMA_DEFAULTS = {
|
|
|
87
89
|
'planner.stall_detect_interval_minutes': 5,
|
|
88
90
|
'planner.stall_threshold_minutes': 10,
|
|
89
91
|
'git.create_tag': true,
|
|
92
|
+
// #5706 (epic #3628): per-repository milestone tagging mode. `single` is
|
|
93
|
+
// exactly the pre-existing behavior; `per_repo` is opt-in.
|
|
94
|
+
'git.repo_tagging': 'single',
|
|
90
95
|
// #1689: per-plan agent_hint executor routing — default-on. A no-op for plans
|
|
91
96
|
// without an agent_hint field, so existing dispatch is byte-identical.
|
|
92
97
|
'workflow.agent_hint_routing': true,
|
|
@@ -191,6 +196,18 @@ function validateKnownConfigKeyPath(keyPath) {
|
|
|
191
196
|
error(`Unknown config key: ${keyPath}. Did you mean ${suggested}?`, ERROR_REASON.CONFIG_INVALID_KEY);
|
|
192
197
|
}
|
|
193
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The key gate `config-set` and `config-new-project` share (#5268): a known
|
|
201
|
+
* misspelling gets its suggestion, and anything else outside the schema is
|
|
202
|
+
* refused with the valid-key list. One function, so the two verbs cannot
|
|
203
|
+
* disagree about which keys they accept or about the error text.
|
|
204
|
+
*/
|
|
205
|
+
function assertKnownConfigKey(cwd, kp) {
|
|
206
|
+
validateKnownConfigKeyPath(kp);
|
|
207
|
+
if (!isValidConfigKey(kp, cwd)) {
|
|
208
|
+
error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>, phase_commit_docs.<phase-id>`, ERROR_REASON.CONFIG_INVALID_KEY);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
194
211
|
/**
|
|
195
212
|
* Is `value` an acceptable `git.protected_branches` list (#3552)?
|
|
196
213
|
*
|
|
@@ -271,9 +288,11 @@ function validateShipPrBodySections(value) {
|
|
|
271
288
|
}
|
|
272
289
|
});
|
|
273
290
|
}
|
|
274
|
-
// ─── Core config operations ───────────────────────────────────────────────────
|
|
275
291
|
/**
|
|
276
|
-
*
|
|
292
|
+
* Plan a fully-materialized config object for a new project. Writes nothing:
|
|
293
|
+
* the user defaults rewrite comes back as data, for the caller to persist once
|
|
294
|
+
* it has decided to write at all (#5268: a `--dry-run` never does, and a
|
|
295
|
+
* refused build never gets that far).
|
|
277
296
|
*
|
|
278
297
|
* Merges (increasing priority):
|
|
279
298
|
* 1. Hardcoded defaults — every key that loadConfig() resolves, plus mode/granularity
|
|
@@ -284,11 +303,10 @@ function validateShipPrBodySections(value) {
|
|
|
284
303
|
* Uses the canonical `git` namespace for branching keys (consistent with VALID_CONFIG_KEYS
|
|
285
304
|
* and the settings workflow). loadConfig() handles both flat and nested formats, so this
|
|
286
305
|
* is backward-compatible with existing projects that have flat keys.
|
|
287
|
-
*
|
|
288
|
-
* Returns a plain object — does NOT write any files.
|
|
289
306
|
*/
|
|
290
|
-
function
|
|
307
|
+
function planNewProjectConfig(userChoices) {
|
|
291
308
|
const choices = userChoices || {};
|
|
309
|
+
let defaultsRewrite = null;
|
|
292
310
|
// #4976: the GSD-owned store resolves exactly as the config loader resolves
|
|
293
311
|
// it (`GSD_HOME || homedir()`), so the defaults.json seeding this project is
|
|
294
312
|
// the one the loader and its #3532 shadow warning read in the same run.
|
|
@@ -319,10 +337,9 @@ function buildNewProjectConfig(userChoices) {
|
|
|
319
337
|
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
|
320
338
|
userDefaults['granularity'] = depthToGranularity[userDefaults['depth']] || userDefaults['depth'];
|
|
321
339
|
delete userDefaults['depth'];
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
catch { /* intentionally empty */ }
|
|
340
|
+
// The migrated value is used for this build whether or not the
|
|
341
|
+
// rewrite is ever persisted.
|
|
342
|
+
defaultsRewrite = { path: globalDefaultsPath, contents: JSON.stringify(userDefaults, null, 2) };
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
345
|
}
|
|
@@ -344,6 +361,7 @@ function buildNewProjectConfig(userChoices) {
|
|
|
344
361
|
git: {
|
|
345
362
|
branching_strategy: CONFIG_DEFAULTS.branching_strategy,
|
|
346
363
|
create_tag: true,
|
|
364
|
+
repo_tagging: 'single',
|
|
347
365
|
phase_branch_template: CONFIG_DEFAULTS.phase_branch_template,
|
|
348
366
|
milestone_branch_template: CONFIG_DEFAULTS.milestone_branch_template,
|
|
349
367
|
quick_branch_template: CONFIG_DEFAULTS.quick_branch_template,
|
|
@@ -444,7 +462,134 @@ function buildNewProjectConfig(userChoices) {
|
|
|
444
462
|
},
|
|
445
463
|
};
|
|
446
464
|
validateShipPrBodySections(config['ship']['pr_body_sections']);
|
|
447
|
-
return config;
|
|
465
|
+
return { config, defaultsRewrite };
|
|
466
|
+
}
|
|
467
|
+
/** Persist a plan's user defaults rewrite, best-effort as it always was. */
|
|
468
|
+
function persistDefaultsRewrite(plan) {
|
|
469
|
+
if (plan.defaultsRewrite === null)
|
|
470
|
+
return;
|
|
471
|
+
try {
|
|
472
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(plan.defaultsRewrite.path, plan.defaultsRewrite.contents);
|
|
473
|
+
}
|
|
474
|
+
catch { /* intentionally empty */ }
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Build a new project's config and persist the user defaults rewrite its plan
|
|
478
|
+
* carries. For callers that write the config unconditionally; one that may not
|
|
479
|
+
* (a `--dry-run`) plans with `planNewProjectConfig` and persists only on write.
|
|
480
|
+
*/
|
|
481
|
+
function buildNewProjectConfig(userChoices) {
|
|
482
|
+
const plan = planNewProjectConfig(userChoices);
|
|
483
|
+
persistDefaultsRewrite(plan);
|
|
484
|
+
return plan.config;
|
|
485
|
+
}
|
|
486
|
+
function isPlainObject(value) {
|
|
487
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Does any schema key live under `keyPath`? True for a section (`workflow`),
|
|
491
|
+
* for the top level of a dynamic key family (`agent_skills`, whose keys are
|
|
492
|
+
* `agent_skills.<agent-type>`), and for the settings that are also parents
|
|
493
|
+
* (`review.max_prompt_tokens_per_reviewer` and its per-reviewer keys;
|
|
494
|
+
* `agent_tools`, a setting whose entries are the `agent_tools.<selector>`
|
|
495
|
+
* family).
|
|
496
|
+
*/
|
|
497
|
+
function hasChildConfigKeys(cwd, keyPath) {
|
|
498
|
+
const childPrefix = `${keyPath}.`;
|
|
499
|
+
for (const known of VALID_CONFIG_KEYS) {
|
|
500
|
+
if (known.startsWith(childPrefix))
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (DYNAMIC_KEY_PATTERNS.some((family) => family['topLevel'] === keyPath))
|
|
504
|
+
return true;
|
|
505
|
+
return Object.keys(getCapabilityConfigSchema(cwd)).some((known) => known.startsWith(childPrefix));
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Is `keyPath` a section a choice may nest settings under? The same test as
|
|
509
|
+
* `hasChildConfigKeys`. A dynamic family's deeper levels
|
|
510
|
+
* (`model_profile_overrides.<runtime>`) are not recognised; an empty object
|
|
511
|
+
* there is refused as an unknown key, and it would set nothing anyway.
|
|
512
|
+
*/
|
|
513
|
+
function isConfigSection(cwd, keyPath) {
|
|
514
|
+
return hasChildConfigKeys(cwd, keyPath);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Hold `config-new-project` choices to the rules `config-set` applies (#5268).
|
|
518
|
+
*
|
|
519
|
+
* Choices arrive nested (`{"workflow":{"research":true}}`) while the schema
|
|
520
|
+
* and `config-set` address dotted leaf paths (`workflow.research`), so the
|
|
521
|
+
* walk descends through section objects and checks each leaf under its dotted
|
|
522
|
+
* path: the key through `assertKnownConfigKey`, the value through
|
|
523
|
+
* `validateConfigValue`. An object whose own path is a settable key with
|
|
524
|
+
* nothing under it (`model_overrides.<agent>`) is a leaf, as it is for
|
|
525
|
+
* `config-set`.
|
|
526
|
+
*
|
|
527
|
+
* Returns the checked choices as a new object and leaves `choices` as given:
|
|
528
|
+
* each value is the one `config-set` would persist (a normalizing rule's
|
|
529
|
+
* result where one applies). An empty object is a section with nothing set:
|
|
530
|
+
* refused as an unknown key unless its name is a real section, and left out
|
|
531
|
+
* of the result either way.
|
|
532
|
+
*/
|
|
533
|
+
function validateNewProjectChoices(cwd, choices, prefix = '') {
|
|
534
|
+
const checked = {};
|
|
535
|
+
for (const key of Object.keys(choices)) {
|
|
536
|
+
const kp = prefix ? `${prefix}.${key}` : key;
|
|
537
|
+
// An empty key names no setting. Refused here because an empty section
|
|
538
|
+
// name would otherwise be walked as if its contents sat one level up.
|
|
539
|
+
if (key === '')
|
|
540
|
+
assertKnownConfigKey(cwd, kp);
|
|
541
|
+
// A key is one path segment. A dot inside it spells a deeper path, which
|
|
542
|
+
// would pass the key gate while the merge wrote it as one literal key no
|
|
543
|
+
// reader resolves. The exception is a dot inside an id: one dynamic key
|
|
544
|
+
// family admits both the path cut at the dot and the whole path, so the
|
|
545
|
+
// dot is part of what that family calls a single key (`phase_commit_docs`
|
|
546
|
+
// is a map keyed by phase id, and "2.1" is a phase id).
|
|
547
|
+
const dot = key.indexOf('.');
|
|
548
|
+
if (dot !== -1) {
|
|
549
|
+
const head = prefix ? `${prefix}.${key.slice(0, dot)}` : key.slice(0, dot);
|
|
550
|
+
const dotIsInsideAnId = prefix !== '' && DYNAMIC_KEY_PATTERNS.some((family) => family.test(head) && family.test(kp));
|
|
551
|
+
if (!dotIsInsideAnId) {
|
|
552
|
+
error(`Invalid choice key "${key}" for config-new-project. Nest it instead of using a dotted key, e.g. {"workflow":{"research":true}}.`, ERROR_REASON.CONFIG_INVALID_KEY);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const value = choices[key];
|
|
556
|
+
if (isPlainObject(value) && (!isValidConfigKey(kp, cwd) || hasChildConfigKeys(cwd, kp))) {
|
|
557
|
+
// A non-empty section has its name checked through its leaves. An empty
|
|
558
|
+
// one has no leaves, so its name is checked here: config-set refuses
|
|
559
|
+
// `bogus '{}'` as an unknown key, and so does this.
|
|
560
|
+
if (Object.keys(value).length === 0 && !isValidConfigKey(kp, cwd) && !isConfigSection(cwd, kp)) {
|
|
561
|
+
assertKnownConfigKey(cwd, kp);
|
|
562
|
+
}
|
|
563
|
+
const section = validateNewProjectChoices(cwd, value, kp);
|
|
564
|
+
// Empty as given, or emptied by null choices: it sets nothing.
|
|
565
|
+
if (Object.keys(section).length > 0)
|
|
566
|
+
checked[key] = section;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
// A section given a value that is not an object. config-set would call
|
|
570
|
+
// the name unknown, which misdescribes a real section; say what is wrong.
|
|
571
|
+
if (!isValidConfigKey(kp, cwd) && isConfigSection(cwd, kp)) {
|
|
572
|
+
error(`Invalid choice "${kp}" for config-new-project: it is a section, so its value must be an object of settings, e.g. {"workflow":{"research":true}}.`, ERROR_REASON.CONFIG_INVALID_KEY);
|
|
573
|
+
}
|
|
574
|
+
assertKnownConfigKey(cwd, kp);
|
|
575
|
+
// `config-set <key> null` clears the key: it skips the typed rules and
|
|
576
|
+
// leaves the key to its default. A null choice is the same request, so it
|
|
577
|
+
// skips them too and is left unchosen, where it used to be written as a
|
|
578
|
+
// literal null that config-set can never store.
|
|
579
|
+
const checkedValue = value === null
|
|
580
|
+
? null
|
|
581
|
+
: validateConfigValue(cwd, kp, value, typeof value === 'string' ? value : JSON.stringify(value));
|
|
582
|
+
// config-set's setter refuses these segments after the key and value
|
|
583
|
+
// rules have run (`_setNestedValue`); same refusal, same place in the order.
|
|
584
|
+
// It also runs before the assignment below, so no such key reaches `checked`.
|
|
585
|
+
if (kp.split('.').some((segment) => segment === '__proto__' || segment === 'prototype' || segment === 'constructor')) {
|
|
586
|
+
error('Invalid config key (prototype pollution guard): ' + kp, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
587
|
+
}
|
|
588
|
+
if (value === null)
|
|
589
|
+
continue;
|
|
590
|
+
checked[key] = checkedValue;
|
|
591
|
+
}
|
|
592
|
+
return checked;
|
|
448
593
|
}
|
|
449
594
|
/**
|
|
450
595
|
* Command: create a fully-materialized .planning/config.json for a new project.
|
|
@@ -453,25 +598,54 @@ function buildNewProjectConfig(userChoices) {
|
|
|
453
598
|
* configured during /gsd:new-project). All remaining keys are filled from
|
|
454
599
|
* hardcoded defaults and optional $GSD_HOME/.gsd/defaults.json.
|
|
455
600
|
*
|
|
456
|
-
*
|
|
601
|
+
* Choices are validated with `config-set`'s key and value rules before
|
|
602
|
+
* anything is written (#5268). With `dryRun`, prints the config that would be
|
|
603
|
+
* created and writes nothing.
|
|
604
|
+
*
|
|
605
|
+
* Idempotent: if config.json already exists, returns { created: false } once
|
|
606
|
+
* the choices have been checked; a refused choice is refused either way.
|
|
457
607
|
*/
|
|
458
|
-
function cmdConfigNewProject(cwd, choicesJson, raw) {
|
|
608
|
+
function cmdConfigNewProject(cwd, choicesJson, raw, options = {}) {
|
|
609
|
+
const dryRun = options.dryRun === true;
|
|
459
610
|
const planningBase = planningDir(cwd);
|
|
460
611
|
const configPath = node_path_1.default.join(planningBase, 'config.json');
|
|
461
|
-
// Idempotent: don't overwrite existing config
|
|
462
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
463
|
-
output({ created: false, reason: 'already_exists' }, raw, 'exists');
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
612
|
// Parse user choices
|
|
467
613
|
let userChoices = {};
|
|
468
614
|
if (choicesJson && choicesJson.trim() !== '') {
|
|
615
|
+
let parsedChoices;
|
|
469
616
|
try {
|
|
470
|
-
|
|
617
|
+
parsedChoices = JSON.parse(choicesJson);
|
|
471
618
|
}
|
|
472
619
|
catch (err) {
|
|
473
620
|
error('Invalid JSON for config-new-project: ' + err.message);
|
|
474
621
|
}
|
|
622
|
+
// #5268: an array or a string parses as JSON and was then spread into the
|
|
623
|
+
// config as index keys.
|
|
624
|
+
if (!isPlainObject(parsedChoices)) {
|
|
625
|
+
error('Invalid choices for config-new-project: expected a JSON object of config keys.', ERROR_REASON.USAGE);
|
|
626
|
+
}
|
|
627
|
+
userChoices = parsedChoices;
|
|
628
|
+
}
|
|
629
|
+
// #5268: refuse what config-set refuses, before the first write — and
|
|
630
|
+
// before the existing-config check, so a refused choice is reported
|
|
631
|
+
// whether or not the project already has a config.
|
|
632
|
+
const checkedChoices = validateNewProjectChoices(cwd, userChoices);
|
|
633
|
+
// Idempotent: don't overwrite existing config
|
|
634
|
+
if (node_fs_1.default.existsSync(configPath)) {
|
|
635
|
+
output({ created: false, reason: 'already_exists' }, raw, 'exists');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const plan = planNewProjectConfig(checkedChoices);
|
|
639
|
+
const config = plan.config;
|
|
640
|
+
if (dryRun) {
|
|
641
|
+
// The preview is CLI output, so secret keys are masked exactly as
|
|
642
|
+
// config-set and config-get mask them. See lib/secrets.cjs.
|
|
643
|
+
const preview = { ...config };
|
|
644
|
+
for (const key of Object.keys(preview)) {
|
|
645
|
+
preview[key] = (0, secrets_cjs_1.maskIfSecret)(key, preview[key]);
|
|
646
|
+
}
|
|
647
|
+
output({ dry_run: true, would_create: true, path: '.planning/config.json', config: preview }, raw, 'would create (dry run)');
|
|
648
|
+
return;
|
|
475
649
|
}
|
|
476
650
|
// Ensure .planning directory exists
|
|
477
651
|
try {
|
|
@@ -480,14 +654,16 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
|
|
|
480
654
|
catch (err) {
|
|
481
655
|
error('Failed to create .planning directory: ' + err.message);
|
|
482
656
|
}
|
|
483
|
-
const config = buildNewProjectConfig(userChoices);
|
|
484
657
|
try {
|
|
485
658
|
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(config, null, 2));
|
|
486
|
-
output({ created: true, path: '.planning/config.json' }, raw, 'created');
|
|
487
659
|
}
|
|
488
660
|
catch (err) {
|
|
489
661
|
error('Failed to write config.json: ' + err.message);
|
|
490
662
|
}
|
|
663
|
+
// The user defaults rewrite follows the config it was planned for: a
|
|
664
|
+
// creation that fails before here leaves the defaults file as it was.
|
|
665
|
+
persistDefaultsRewrite(plan);
|
|
666
|
+
output({ created: true, path: '.planning/config.json' }, raw, 'created');
|
|
491
667
|
}
|
|
492
668
|
/**
|
|
493
669
|
* Ensures the config file exists (creates it if needed).
|
|
@@ -772,10 +948,7 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
|
|
|
772
948
|
// TypeScript doesn't always infer never-return narrowing through error(), so we assert.
|
|
773
949
|
const kp = keyPath;
|
|
774
950
|
const val = value;
|
|
775
|
-
|
|
776
|
-
if (!isValidConfigKey(kp, cwd)) {
|
|
777
|
-
error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>, phase_commit_docs.<phase-id>`, ERROR_REASON.CONFIG_INVALID_KEY);
|
|
778
|
-
}
|
|
951
|
+
assertKnownConfigKey(cwd, kp);
|
|
779
952
|
// Parse value (handle booleans, numbers, and JSON arrays/objects)
|
|
780
953
|
let parsedValue = val;
|
|
781
954
|
if (val === 'true')
|
|
@@ -832,6 +1005,60 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
|
|
|
832
1005
|
if (kp === 'project_code') {
|
|
833
1006
|
parsedValue = val;
|
|
834
1007
|
}
|
|
1008
|
+
parsedValue = validateConfigValue(cwd, kp, parsedValue, val);
|
|
1009
|
+
if (dryRun) {
|
|
1010
|
+
const preview = previewConfigValue(cwd, kp, parsedValue);
|
|
1011
|
+
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
1012
|
+
const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
|
|
1013
|
+
const maskedPrev = preview.previousValue === undefined
|
|
1014
|
+
? undefined
|
|
1015
|
+
: (0, secrets_cjs_1.maskSecret)(preview.previousValue);
|
|
1016
|
+
output({ dry_run: true, would_update: true, key: kp, value: masked, previousValue: maskedPrev, masked: true }, raw, `${kp}=${masked} (dry run)`);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
output({ dry_run: true, would_update: true, key: kp, value: parsedValue, previousValue: preview.previousValue }, raw, `${kp}=${String(parsedValue)} (dry run)`);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
|
|
1023
|
+
// Mask secrets in both JSON and text output. The plaintext is written
|
|
1024
|
+
// to config.json (that's where secrets live on disk); the CLI output
|
|
1025
|
+
// must never echo it. See lib/secrets.cjs.
|
|
1026
|
+
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
1027
|
+
// parsedValue is unknown at this point; maskSecret accepts MaskableValue
|
|
1028
|
+
const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
|
|
1029
|
+
const maskedPrev = setConfigValueResult.previousValue === undefined
|
|
1030
|
+
? undefined
|
|
1031
|
+
: (0, secrets_cjs_1.maskSecret)(setConfigValueResult.previousValue);
|
|
1032
|
+
const maskedResult = {
|
|
1033
|
+
...setConfigValueResult,
|
|
1034
|
+
value: masked,
|
|
1035
|
+
previousValue: maskedPrev,
|
|
1036
|
+
masked: true,
|
|
1037
|
+
};
|
|
1038
|
+
output(maskedResult, raw, `${kp}=${masked}`);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
output(setConfigValueResult, raw, `${kp}=${String(parsedValue)}`);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* The per-key VALUE rules, shared by `config-set` and `config-new-project`
|
|
1045
|
+
* (#5268). They were inline in `cmdConfigSet`, so `config-new-project` had no
|
|
1046
|
+
* way to apply them and wrote values `config-set` refuses for the same key.
|
|
1047
|
+
*
|
|
1048
|
+
* `parsedValue` is the typed value about to be persisted and `val` its text
|
|
1049
|
+
* form, used only in error messages. Returns the value to persist for `kp`:
|
|
1050
|
+
* the input itself, or its normalized form where a rule normalizes
|
|
1051
|
+
* (`review.default_reviewers`). A value the key does not accept has no value
|
|
1052
|
+
* to persist, so it is refused through `error()` instead.
|
|
1053
|
+
*/
|
|
1054
|
+
function validateConfigValue(cwd, kp, parsedValue, val) {
|
|
1055
|
+
// #1581: project_code is an identifier, and config-set stores it as the text
|
|
1056
|
+
// typed. Through config-set this never fires; it holds a JSON choice (#5268)
|
|
1057
|
+
// to the type config-set stores, rather than writing a number readers cast
|
|
1058
|
+
// to string.
|
|
1059
|
+
if (kp === 'project_code' && typeof parsedValue !== 'string') {
|
|
1060
|
+
error(`Invalid project_code '${val}'. Must be a string identifier, e.g. "PROJ".`);
|
|
1061
|
+
}
|
|
835
1062
|
const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
|
|
836
1063
|
if (kp === 'context')
|
|
837
1064
|
assertEnumValue(parsedValue, val, VALID_CONTEXT_VALUES, 'context value');
|
|
@@ -912,6 +1139,9 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
|
|
|
912
1139
|
error(`Invalid git.create_tag '${val}'. Must be a boolean (true or false).`);
|
|
913
1140
|
}
|
|
914
1141
|
}
|
|
1142
|
+
// #5706 (epic #3628): per-repository milestone tagging mode, enum only.
|
|
1143
|
+
if (kp === 'git.repo_tagging')
|
|
1144
|
+
assertEnumValue(parsedValue, val, VALID_REPO_TAGGING_MODES, 'git.repo_tagging');
|
|
915
1145
|
if (kp === 'git.protected_branches') {
|
|
916
1146
|
if (!isValidProtectedBranches(parsedValue)) {
|
|
917
1147
|
error(`Invalid git.protected_branches '${val}'. Must be a non-empty array of non-empty branch names.`);
|
|
@@ -1076,39 +1306,7 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
|
|
|
1076
1306
|
}
|
|
1077
1307
|
}
|
|
1078
1308
|
}
|
|
1079
|
-
|
|
1080
|
-
const preview = previewConfigValue(cwd, kp, parsedValue);
|
|
1081
|
-
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
1082
|
-
const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
|
|
1083
|
-
const maskedPrev = preview.previousValue === undefined
|
|
1084
|
-
? undefined
|
|
1085
|
-
: (0, secrets_cjs_1.maskSecret)(preview.previousValue);
|
|
1086
|
-
output({ dry_run: true, would_update: true, key: kp, value: masked, previousValue: maskedPrev, masked: true }, raw, `${kp}=${masked} (dry run)`);
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
output({ dry_run: true, would_update: true, key: kp, value: parsedValue, previousValue: preview.previousValue }, raw, `${kp}=${String(parsedValue)} (dry run)`);
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
|
|
1093
|
-
// Mask secrets in both JSON and text output. The plaintext is written
|
|
1094
|
-
// to config.json (that's where secrets live on disk); the CLI output
|
|
1095
|
-
// must never echo it. See lib/secrets.cjs.
|
|
1096
|
-
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
1097
|
-
// parsedValue is unknown at this point; maskSecret accepts MaskableValue
|
|
1098
|
-
const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
|
|
1099
|
-
const maskedPrev = setConfigValueResult.previousValue === undefined
|
|
1100
|
-
? undefined
|
|
1101
|
-
: (0, secrets_cjs_1.maskSecret)(setConfigValueResult.previousValue);
|
|
1102
|
-
const maskedResult = {
|
|
1103
|
-
...setConfigValueResult,
|
|
1104
|
-
value: masked,
|
|
1105
|
-
previousValue: maskedPrev,
|
|
1106
|
-
masked: true,
|
|
1107
|
-
};
|
|
1108
|
-
output(maskedResult, raw, `${kp}=${masked}`);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
output(setConfigValueResult, raw, `${kp}=${String(parsedValue)}`);
|
|
1309
|
+
return parsedValue;
|
|
1112
1310
|
}
|
|
1113
1311
|
function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
1114
1312
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
@@ -40,6 +40,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
40
40
|
const node_path_1 = __importDefault(require("node:path"));
|
|
41
41
|
// frontmatter-fence.cjs is a leaf module (no imports), so this adds no cycle.
|
|
42
42
|
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
43
|
+
// pattern.cjs is a leaf module (escapeRegex only), so this adds no cycle.
|
|
44
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
43
45
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
46
|
const phaseIdModule = require("./phase-id.cjs");
|
|
45
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -521,6 +523,36 @@ function findOrphanSummaries(planFiles, summaryFiles) {
|
|
|
521
523
|
}
|
|
522
524
|
return summaryFiles.filter((s) => !claimed.has(s));
|
|
523
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* #5542: does `summaryFile` pair with ANY of `planFiles`, by the same
|
|
528
|
+
* `summaryCandidates` rule the counts use? The ROADMAP plan-checkbox writers
|
|
529
|
+
* tick a row only for a summary that does — never by id prefix, which let a
|
|
530
|
+
* leftover `47-1` summary survive because a live `47-10` plan starts with `47-1`.
|
|
531
|
+
*/
|
|
532
|
+
function summaryPairsWithAnyPlan(planFiles, summaryFile) {
|
|
533
|
+
return planFiles.some((plan) => countMatchedSummaries([plan], [summaryFile]) > 0);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* #5542: lookahead that ends a plan id inside a ROADMAP checkbox row, so `47-1`
|
|
537
|
+
* never matches the start of `47-10-PLAN.md`. The id ends at `.md` or at any
|
|
538
|
+
* character that is not a word character or a `.`/`-` followed by a digit: a
|
|
539
|
+
* dotted sub-id (`01-01.1`) or a longer numeric id (`05-01-02`) is a distinct
|
|
540
|
+
* plan, not a continuation of `01-01`/`05-01`, while sentence punctuation
|
|
541
|
+
* (`01-01. Setup`) and a slug (`05-01-scaffold`) still end the id.
|
|
542
|
+
*/
|
|
543
|
+
const PLAN_ID_ROW_BOUNDARY_SOURCE = '(?=\\.md|(?!\\w|[.-]\\d))';
|
|
544
|
+
/**
|
|
545
|
+
* #5542: the checkbox-row regex for one plan id. By default it matches an
|
|
546
|
+
* UNCHECKED row (the tick target); `anyState` matches either box state, for
|
|
547
|
+
* presence detection; `anchored` pins the row to a line start (the
|
|
548
|
+
* `phase complete` bullet flip). Capture group 1 is the opening `- [`, group 2
|
|
549
|
+
* the rest after the box, so a caller ticks with `$1x$2`.
|
|
550
|
+
*/
|
|
551
|
+
function planRowPattern(planId, opts = {}) {
|
|
552
|
+
const box = opts.anyState ? '[x ]' : ' ';
|
|
553
|
+
const lead = opts.anchored ? '^(\\s*-\\s*\\[)' : '(-\\s*\\[)';
|
|
554
|
+
return new RegExp(`${lead}${box}(\\]\\s*(?:\\*\\*)?${(0, pattern_cjs_1.escapeRegex)(planId)}${PLAN_ID_ROW_BOUNDARY_SOURCE}(?:\\*\\*)?)`, 'i');
|
|
555
|
+
}
|
|
524
556
|
module.exports = {
|
|
525
557
|
toPosixPath,
|
|
526
558
|
normalizeLineEndings,
|
|
@@ -536,4 +568,6 @@ module.exports = {
|
|
|
536
568
|
countMatchedSummaries,
|
|
537
569
|
findUnsummarizedPlans,
|
|
538
570
|
findOrphanSummaries,
|
|
571
|
+
summaryPairsWithAnyPlan,
|
|
572
|
+
planRowPattern,
|
|
539
573
|
};
|