@opengsd/gsd-core 1.7.0 → 1.9.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 +1 -1
- package/.opencode/plugins/gsd-core.js +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -19,6 +19,8 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
|
|
|
19
19
|
// providing a deterministic failure path when git stalls (locked index, hung
|
|
20
20
|
// remote, stalled NFS mount, etc.). Callers can override via deps.timeout.
|
|
21
21
|
const DEFAULT_GIT_TIMEOUT_MS = 10000;
|
|
22
|
+
const WORKTREE_AGENT_BRANCH_RE = /^(worktree-)?agent-[A-Za-z0-9._/-]+$/;
|
|
23
|
+
const WORKTREE_AGENT_BRANCH_PATTERN = WORKTREE_AGENT_BRANCH_RE.source;
|
|
22
24
|
/**
|
|
23
25
|
* Execute a git command via the shell-projection seam, with a derived
|
|
24
26
|
* `timedOut` field. Tests inject mocks via deps.execGit using the new
|
|
@@ -302,7 +304,7 @@ function normalizeCleanupManifestEntry(entry) {
|
|
|
302
304
|
const expectedBase = typeof e.expected_base === 'string' ? e.expected_base : '';
|
|
303
305
|
if (!worktreePath || !branch || !expectedBase)
|
|
304
306
|
return null;
|
|
305
|
-
if (
|
|
307
|
+
if (!WORKTREE_AGENT_BRANCH_RE.test(branch))
|
|
306
308
|
return null;
|
|
307
309
|
const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
|
|
308
310
|
const allowedBases = Array.from(new Set([expectedBase, ...rawAllowedBases.filter((base) => typeof base === 'string' && base.length > 0)]));
|
|
@@ -449,20 +451,18 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
|
|
|
449
451
|
// the executor's content could be lost. cat-file -e HEAD:<path> returns
|
|
450
452
|
// exit 0 only when the object exists in the committed HEAD tree.
|
|
451
453
|
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
454
|
+
// #2556: rescue whenever the object is NOT confirmed committed (any non-zero
|
|
455
|
+
// exit). `git cat-file -e` returns 128 — NOT 1 — for an absent path (the
|
|
456
|
+
// normal uncommitted-SUMMARY state), so the previous `!== 1` check never
|
|
457
|
+
// rescued and the untracked file was silently discarded by `worktree remove
|
|
458
|
+
// --force`. Data safety wins: an un-rescued untracked SUMMARY is lost, while
|
|
459
|
+
// a spurious rescue is usually a no-op — the destination check below skips the
|
|
460
|
+
// copy when the main tree already holds identical content (which is also what
|
|
461
|
+
// guards the #706 merge collision). A divergent dest is overwritten, but only
|
|
462
|
+
// uncommitted main-tree content could be lost (committed content is git-recoverable).
|
|
457
463
|
const catFileResult = execGit(['-C', worktreePath, 'cat-file', '-e', `HEAD:${relPath}`], { cwd: repoRoot });
|
|
458
|
-
if (catFileResult.exitCode
|
|
459
|
-
//
|
|
460
|
-
// exit 0 → object exists (committed on HEAD) — merge will carry it, skip.
|
|
461
|
-
// exit 128 → fatal git error (corrupt store, unborn HEAD, etc.) — uncertain,
|
|
462
|
-
// fail-closed: do NOT rescue to avoid recreating the #706 collision.
|
|
463
|
-
// timedOut / null / other → unreliable result — same fail-closed policy.
|
|
464
|
-
// In all non-1 cases the merge will either succeed naturally (0) or surface
|
|
465
|
-
// the problem safely (128/timeout), which is the recoverable pre-fix behaviour.
|
|
464
|
+
if (catFileResult.exitCode === 0) {
|
|
465
|
+
// exit 0 → the SUMMARY is committed on HEAD; the merge will carry it, so skip rescue.
|
|
466
466
|
continue;
|
|
467
467
|
}
|
|
468
468
|
const dest = node_path_1.default.join(repoRoot, relPath);
|
|
@@ -751,7 +751,7 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
|
|
|
751
751
|
return {
|
|
752
752
|
ok: false,
|
|
753
753
|
reason: 'invalid_entry',
|
|
754
|
-
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match
|
|
754
|
+
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts both agent-<id> and worktree-agent-<id> namespaces; got branch="${branch}"). Fix the field and re-run.`,
|
|
755
755
|
entry: null,
|
|
756
756
|
manifest: null,
|
|
757
757
|
};
|
|
@@ -893,6 +893,348 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
|
|
|
893
893
|
write(`${JSON.stringify({ ok: true, reason: 'ok', entry: plan.entry, manifest_path: resolved }, null, 2)}\n`);
|
|
894
894
|
return { ok: true, reason: 'ok', entry: plan.entry, manifest_path: resolved };
|
|
895
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Pure planner for `worktree create`. Validates the four required fields
|
|
898
|
+
* (write-strict, same missing-field-hint style as `planWorktreeRecordAgent`),
|
|
899
|
+
* then runs the candidate entry through the SAME `normalizeCleanupManifestEntry`
|
|
900
|
+
* validation the cleanup-wave reader and record-agent use — so a worktree this
|
|
901
|
+
* verb creates is guaranteed manageable by cleanup-wave/reap-orphans, and an
|
|
902
|
+
* entry that would fail the reader's branch-namespace guard is rejected here,
|
|
903
|
+
* fail-closed, before any git command runs.
|
|
904
|
+
*/
|
|
905
|
+
function planWorktreeCreate(fields) {
|
|
906
|
+
const agentId = (fields.agentId || '').trim();
|
|
907
|
+
const worktreePath = (fields.worktreePath || '').trim();
|
|
908
|
+
const branch = (fields.branch || '').trim();
|
|
909
|
+
const base = (fields.base || '').trim();
|
|
910
|
+
const missing = [];
|
|
911
|
+
if (!agentId)
|
|
912
|
+
missing.push('--agent-id');
|
|
913
|
+
if (!worktreePath)
|
|
914
|
+
missing.push('--path');
|
|
915
|
+
if (!branch)
|
|
916
|
+
missing.push('--branch');
|
|
917
|
+
if (!base)
|
|
918
|
+
missing.push('--base');
|
|
919
|
+
if (missing.length > 0) {
|
|
920
|
+
return {
|
|
921
|
+
ok: false,
|
|
922
|
+
reason: 'missing_field',
|
|
923
|
+
hint: `worktree create requires ${missing.join(', ')}. Re-run with all of --agent-id, --path, --branch, --base set to non-empty (non-whitespace) values.`,
|
|
924
|
+
entry: null,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const candidate = {
|
|
928
|
+
agent_id: agentId,
|
|
929
|
+
worktree_path: worktreePath,
|
|
930
|
+
branch,
|
|
931
|
+
expected_base: base,
|
|
932
|
+
};
|
|
933
|
+
const entry = normalizeCleanupManifestEntry(candidate);
|
|
934
|
+
if (!entry) {
|
|
935
|
+
return {
|
|
936
|
+
ok: false,
|
|
937
|
+
reason: 'invalid_entry',
|
|
938
|
+
hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts both agent-<id> and worktree-agent-<id> namespaces; got branch="${branch}"). Fix the field and re-run.`,
|
|
939
|
+
entry: null,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
// #2584 FIX 4 — git argument-injection guard: a value starting with '-'
|
|
943
|
+
// could be parsed by git as a FLAG rather than a positional argument (e.g.
|
|
944
|
+
// base="--upload-pack=x", path="-f"). `git worktree add` / `git rev-parse`
|
|
945
|
+
// support for a `--` end-of-options separator is inconsistent across git
|
|
946
|
+
// versions, so rejecting a leading dash outright — not relying on `--` — is
|
|
947
|
+
// the portable fix.
|
|
948
|
+
if (branch.startsWith('-') || base.startsWith('-') || worktreePath.startsWith('-')) {
|
|
949
|
+
return {
|
|
950
|
+
ok: false,
|
|
951
|
+
reason: 'unsafe_leading_dash',
|
|
952
|
+
hint: `--branch/--base/--path must not start with "-" (a leading dash would be parsed by git as a flag, not a value). Got branch="${branch}" base="${base}" path="${worktreePath}".`,
|
|
953
|
+
entry: null,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
// #2584 FIX 4 — path-traversal guard: reject a ".." path segment in --path.
|
|
957
|
+
// Absolute paths ARE allowed (the orchestrator legitimately uses them —
|
|
958
|
+
// Phase-3 root confinement is out of Phase-2 scope); only a literal ".."
|
|
959
|
+
// component is rejected. Split on BOTH separators so the guard is effective
|
|
960
|
+
// on a Windows-style path too.
|
|
961
|
+
if (worktreePath.split(/[/\\]/).includes('..')) {
|
|
962
|
+
return {
|
|
963
|
+
ok: false,
|
|
964
|
+
reason: 'unsafe_path_traversal',
|
|
965
|
+
hint: `--path must not contain a ".." path segment (got: "${worktreePath}").`,
|
|
966
|
+
entry: null,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
return { ok: true, reason: 'ok', entry };
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Best-effort bounded rollback of a partial/orphaned worktree (#2584 FIX 3,
|
|
973
|
+
* scope narrowed by FIX 5). Invoked ONLY when a `git worktree add` TIMED OUT
|
|
974
|
+
* mid-operation — a SIGTERM'd `add` can leave a `.git/worktrees/<name>` admin
|
|
975
|
+
* entry / directory on disk that got past validation into the file checkout,
|
|
976
|
+
* so the partial is genuinely THIS call's own creation and is safe to
|
|
977
|
+
* best-effort remove immediately. It is deliberately NOT invoked on a clean
|
|
978
|
+
* non-zero `add` exit (see FIX 5) — the most common such failure is a
|
|
979
|
+
* COLLISION (the path/branch is already a registered worktree), git fails
|
|
980
|
+
* FAST there having created nothing, and the branch namespace this verb
|
|
981
|
+
* writes into (`worktree-agent-*`/`agent-*`) is exactly the concurrent-
|
|
982
|
+
* executor namespace, so a colliding path is very plausibly a LIVE PEER
|
|
983
|
+
* executor whose uncommitted work `--force` would destroy. Also invoked from
|
|
984
|
+
* `cmdWorktreeCreate` when a successful `add` is followed by a manifest-write
|
|
985
|
+
* failure (#2584 FIX 1) — that path proves THIS call created the worktree, so
|
|
986
|
+
* removing it is safe. This is immediate best-effort hygiene, not the only
|
|
987
|
+
* safety net: `reapOrphanWorktrees` scans the `.git/worktrees/` admin
|
|
988
|
+
* directory directly (a genuine directory-scan backstop, not manifest-only),
|
|
989
|
+
* so any partial this call cannot reach is still eventually discovered and
|
|
990
|
+
* reaped there. The result is intentionally ignored and a throw is
|
|
991
|
+
* swallowed: this is best-effort cleanup, never a new source of truth, and
|
|
992
|
+
* must never mask or block the caller's own degraded-but-honest return.
|
|
993
|
+
*/
|
|
994
|
+
function rollbackPartialWorktree(execGit, worktreePath, repoRoot) {
|
|
995
|
+
try {
|
|
996
|
+
execGit(['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot });
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// best-effort only — a throwing rollback must never mask the original failure.
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Execute a `planWorktreeCreate` plan via bounded git. Fail-closed at every
|
|
1004
|
+
* step — a timeout or a non-zero exit degrades to a structured result rather
|
|
1005
|
+
* than throwing, and the base must resolve BEFORE any worktree is created (no
|
|
1006
|
+
* partial/orphaned worktree on a bad base). Returns `cwd` — the working
|
|
1007
|
+
* directory Phase 3's executor spawn will pass through.
|
|
1008
|
+
*/
|
|
1009
|
+
function executeWorktreeCreatePlan(plan, repoRoot, deps = {}) {
|
|
1010
|
+
const execGit = deps.execGit || execGitDefault;
|
|
1011
|
+
if (!plan || !plan.ok || !plan.entry) {
|
|
1012
|
+
return {
|
|
1013
|
+
ok: false,
|
|
1014
|
+
reason: plan ? plan.reason : 'missing_plan',
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const { worktree_path: worktreePath, branch, expected_base: base } = plan.entry;
|
|
1018
|
+
const normalizedPath = (0, shell_command_projection_cjs_1.posixNormalize)(worktreePath);
|
|
1019
|
+
// 1. Verify the base resolves BEFORE creating anything (fail-closed). Nothing
|
|
1020
|
+
// has been created on this path yet, so there is nothing to roll back.
|
|
1021
|
+
const baseCheck = execGit(['rev-parse', '--verify', '--quiet', `${base}^{commit}`], { cwd: repoRoot });
|
|
1022
|
+
if (baseCheck.timedOut) {
|
|
1023
|
+
return { ok: false, reason: 'git_timeout', worktree_path: normalizedPath, branch, base };
|
|
1024
|
+
}
|
|
1025
|
+
if (!gitResultOk(baseCheck)) {
|
|
1026
|
+
return { ok: false, reason: 'base_unresolved', worktree_path: normalizedPath, branch, base, stderr: baseCheck.stderr || '' };
|
|
1027
|
+
}
|
|
1028
|
+
// 2. Create the worktree + branch together.
|
|
1029
|
+
const addResult = execGit(['worktree', 'add', '-b', branch, worktreePath, base], { cwd: repoRoot });
|
|
1030
|
+
if (addResult.timedOut) {
|
|
1031
|
+
// #2584 FIX 3: a SIGTERM'd `add` can leave a partial worktree on disk.
|
|
1032
|
+
rollbackPartialWorktree(execGit, worktreePath, repoRoot);
|
|
1033
|
+
return { ok: false, reason: 'git_timeout', worktree_path: normalizedPath, branch, base };
|
|
1034
|
+
}
|
|
1035
|
+
if (addResult.exitCode !== 0) {
|
|
1036
|
+
// #2584 FIX 5: deliberately NO rollback here. A clean non-zero exit is
|
|
1037
|
+
// most commonly a COLLISION (path/branch already a registered worktree),
|
|
1038
|
+
// and git fails FAST on that — it creates nothing. A colliding path in
|
|
1039
|
+
// this branch namespace is very plausibly a LIVE PEER executor;
|
|
1040
|
+
// `git worktree remove --force` on it would destroy real, uncommitted
|
|
1041
|
+
// work. The safe response to a clean failure is to fail loudly and leave
|
|
1042
|
+
// whatever is already on disk untouched.
|
|
1043
|
+
return { ok: false, reason: 'worktree_add_failed', worktree_path: normalizedPath, branch, base, stderr: addResult.stderr || '' };
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
ok: true,
|
|
1047
|
+
reason: 'created',
|
|
1048
|
+
worktree_path: normalizedPath,
|
|
1049
|
+
branch,
|
|
1050
|
+
base,
|
|
1051
|
+
cwd: normalizedPath,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* CLI command: create a git worktree + branch off `--base`, then append the
|
|
1056
|
+
* validated manifest entry so the worktree is immediately manageable by
|
|
1057
|
+
* `worktree cleanup-wave` / `worktree reap-orphans`.
|
|
1058
|
+
*
|
|
1059
|
+
* Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
|
|
1060
|
+
*
|
|
1061
|
+
* #2584 FIX 1 — ORDERING CONTRACT: every manifest read/parse/shape-validate/
|
|
1062
|
+
* plan step runs BEFORE the git side effect (step 5). The ONLY manifest
|
|
1063
|
+
* operation that can run AFTER `git worktree add` has succeeded is the final
|
|
1064
|
+
* guarded write (step 6), and a failure there triggers a best-effort rollback
|
|
1065
|
+
* of the just-created worktree — so a malformed/mis-shaped manifest, or a
|
|
1066
|
+
* `writeFile` IO error, can never leave a REAL worktree on disk with no
|
|
1067
|
+
* manifest entry (cleanup-wave/reap-orphans only discover worktrees via the
|
|
1068
|
+
* manifest, never a directory scan) or an uncaught throw.
|
|
1069
|
+
*/
|
|
1070
|
+
function cmdWorktreeCreate(cwd, args = [], deps = {}) {
|
|
1071
|
+
const flag = (name) => {
|
|
1072
|
+
const i = args.indexOf(name);
|
|
1073
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : '';
|
|
1074
|
+
};
|
|
1075
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
1076
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
|
|
1077
|
+
const manifestPath = flag('--manifest');
|
|
1078
|
+
if (!manifestPath) {
|
|
1079
|
+
writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--root <dir>]\n');
|
|
1080
|
+
process.exitCode = 2;
|
|
1081
|
+
return { ok: false, reason: 'usage' };
|
|
1082
|
+
}
|
|
1083
|
+
// 1. Read the manifest (no side effect yet).
|
|
1084
|
+
const resolved = node_path_1.default.resolve(cwd, manifestPath);
|
|
1085
|
+
const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
|
|
1086
|
+
let manifestRaw;
|
|
1087
|
+
try {
|
|
1088
|
+
manifestRaw = readFile(resolved);
|
|
1089
|
+
}
|
|
1090
|
+
catch (err) {
|
|
1091
|
+
const hint = `Manifest not found or unreadable at ${manifestPath}. The orchestrator must initialize it ({"orchestrator_root": "...", "worktrees": []}) before creating agent worktrees.`;
|
|
1092
|
+
writeErr(`[gsd] worktree.create: manifest_read_failed — ${hint}\n`);
|
|
1093
|
+
write(`${JSON.stringify({ ok: false, reason: 'manifest_read_failed', hint, error: err.message }, null, 2)}\n`);
|
|
1094
|
+
process.exitCode = 1;
|
|
1095
|
+
return { ok: false, reason: 'manifest_read_failed', hint };
|
|
1096
|
+
}
|
|
1097
|
+
// 2. Parse + shape-validate the manifest BEFORE any git command runs.
|
|
1098
|
+
// Mirrors planWorktreeRecordAgent's shell-acceptance rules (canonical
|
|
1099
|
+
// {worktrees:[]} object OR a bare top-level array).
|
|
1100
|
+
let parsed;
|
|
1101
|
+
try {
|
|
1102
|
+
parsed = JSON.parse(manifestRaw);
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
const hint = 'Manifest is not valid JSON. The orchestrator must initialize it as {"orchestrator_root": "...", "worktrees": []} before creating agent worktrees.';
|
|
1106
|
+
writeErr(`[gsd] worktree.create: invalid_manifest_json — ${hint}\n`);
|
|
1107
|
+
write(`${JSON.stringify({ ok: false, reason: 'invalid_manifest_json', hint }, null, 2)}\n`);
|
|
1108
|
+
process.exitCode = 1;
|
|
1109
|
+
return { ok: false, reason: 'invalid_manifest_json', hint };
|
|
1110
|
+
}
|
|
1111
|
+
let worktrees;
|
|
1112
|
+
let writeBack;
|
|
1113
|
+
if (Array.isArray(parsed)) {
|
|
1114
|
+
worktrees = parsed;
|
|
1115
|
+
writeBack = worktrees;
|
|
1116
|
+
}
|
|
1117
|
+
else if (parsed && typeof parsed === 'object') {
|
|
1118
|
+
const container = parsed;
|
|
1119
|
+
if (container.worktrees === undefined)
|
|
1120
|
+
container.worktrees = [];
|
|
1121
|
+
if (!Array.isArray(container.worktrees)) {
|
|
1122
|
+
const hint = 'Manifest "worktrees" must be an array. Re-initialize as {"orchestrator_root": "...", "worktrees": []}.';
|
|
1123
|
+
writeErr(`[gsd] worktree.create: manifest_shape_invalid — ${hint}\n`);
|
|
1124
|
+
write(`${JSON.stringify({ ok: false, reason: 'manifest_shape_invalid', hint }, null, 2)}\n`);
|
|
1125
|
+
process.exitCode = 1;
|
|
1126
|
+
return { ok: false, reason: 'manifest_shape_invalid', hint };
|
|
1127
|
+
}
|
|
1128
|
+
worktrees = container.worktrees;
|
|
1129
|
+
writeBack = container;
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
const hint = 'Manifest must be a JSON object {"worktrees": []} or a top-level array.';
|
|
1133
|
+
writeErr(`[gsd] worktree.create: manifest_shape_invalid — ${hint}\n`);
|
|
1134
|
+
write(`${JSON.stringify({ ok: false, reason: 'manifest_shape_invalid', hint }, null, 2)}\n`);
|
|
1135
|
+
process.exitCode = 1;
|
|
1136
|
+
return { ok: false, reason: 'manifest_shape_invalid', hint };
|
|
1137
|
+
}
|
|
1138
|
+
// 3. Plan (pure, no I/O) — still before any git command.
|
|
1139
|
+
const plan = planWorktreeCreate({
|
|
1140
|
+
agentId: flag('--agent-id'),
|
|
1141
|
+
worktreePath: flag('--path'),
|
|
1142
|
+
branch: flag('--branch'),
|
|
1143
|
+
base: flag('--base'),
|
|
1144
|
+
});
|
|
1145
|
+
if (!plan.ok || !plan.entry) {
|
|
1146
|
+
writeErr(`[gsd] worktree.create: ${plan.reason} — ${plan.hint || ''}\n`);
|
|
1147
|
+
write(`${JSON.stringify({ ok: false, reason: plan.reason, hint: plan.hint }, null, 2)}\n`);
|
|
1148
|
+
process.exitCode = 1;
|
|
1149
|
+
return { ok: false, reason: plan.reason, hint: plan.hint };
|
|
1150
|
+
}
|
|
1151
|
+
// 3b. Optional root confinement (#2627, Phase 3 — the confinement Phase 2
|
|
1152
|
+
// deferred here from planWorktreeCreate's path-traversal guard).
|
|
1153
|
+
// planWorktreeCreate rejects a literal ".." SEGMENT, but a plain absolute
|
|
1154
|
+
// path outside the project contains no ".." and passes. Phase 3 makes the
|
|
1155
|
+
// orchestrator SPAWN executor processes into these paths, so an
|
|
1156
|
+
// unconfined --path is a write primitive aimed anywhere on the filesystem.
|
|
1157
|
+
//
|
|
1158
|
+
// The root is DECLARED by the caller (`--root`) rather than inferred: agent
|
|
1159
|
+
// worktrees legitimately live outside the orchestrator's own root (a lane
|
|
1160
|
+
// orchestrator creates siblings under the repo's .claude/worktrees/), so
|
|
1161
|
+
// there is no layout this module could derive without guessing. Absent
|
|
1162
|
+
// `--root` the behavior is exactly as shipped in Phase 2 — the
|
|
1163
|
+
// orchestrator-worktree scheduler path always passes it.
|
|
1164
|
+
//
|
|
1165
|
+
// Lexical by design: the worktree does not exist yet, so there is nothing
|
|
1166
|
+
// to realpath, and resolving only the root would not close a symlinked-leaf
|
|
1167
|
+
// hole. Pairs with the leading-dash and ".."-segment guards above.
|
|
1168
|
+
const rootFlag = flag('--root');
|
|
1169
|
+
if (rootFlag) {
|
|
1170
|
+
const absRoot = node_path_1.default.resolve(cwd, rootFlag);
|
|
1171
|
+
const absWorktree = node_path_1.default.resolve(cwd, plan.entry.worktree_path);
|
|
1172
|
+
const rel = node_path_1.default.relative(absRoot, absWorktree);
|
|
1173
|
+
// rel === '' → the worktree IS the root (would clobber the checkout)
|
|
1174
|
+
// rel === '..' / '../…' → escapes the root
|
|
1175
|
+
// path.isAbsolute(rel) → a different Windows drive or UNC root
|
|
1176
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(rel)) {
|
|
1177
|
+
const hint = `--path must resolve INSIDE --root (root="${absRoot}", path="${absWorktree}"). A worktree outside the declared root is unreachable by manifest-scoped cleanup and would let a spawned executor write outside the project.`;
|
|
1178
|
+
writeErr(`[gsd] worktree.create: path_outside_root — ${hint}\n`);
|
|
1179
|
+
write(`${JSON.stringify({ ok: false, reason: 'path_outside_root', hint }, null, 2)}\n`);
|
|
1180
|
+
process.exitCode = 1;
|
|
1181
|
+
return { ok: false, reason: 'path_outside_root', hint };
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// 4. Compute the deduped final manifest STRING in memory now — the ONLY
|
|
1185
|
+
// manifest work left is the guarded write in step 6, after git succeeds.
|
|
1186
|
+
// #2584 FIX 2: the on-disk entry is the SAME minimal 4-field shape
|
|
1187
|
+
// `cmdWorktreeRecordAgent` writes — never the full normalized entry with
|
|
1188
|
+
// the derived `allowed_bases` — so the two verbs never write divergent
|
|
1189
|
+
// shapes into the same manifest (the reader re-derives `allowed_bases`
|
|
1190
|
+
// from `expected_base` on load, exactly as it does for record-agent).
|
|
1191
|
+
const recorded = {
|
|
1192
|
+
agent_id: plan.entry.agent_id,
|
|
1193
|
+
worktree_path: plan.entry.worktree_path,
|
|
1194
|
+
branch: plan.entry.branch,
|
|
1195
|
+
expected_base: plan.entry.expected_base,
|
|
1196
|
+
};
|
|
1197
|
+
const dedupeKey = `${recorded.worktree_path}\0${recorded.branch}`;
|
|
1198
|
+
const alreadyPresent = worktrees.some((existing) => {
|
|
1199
|
+
const normalized = normalizeCleanupManifestEntry(existing);
|
|
1200
|
+
return normalized !== null && `${normalized.worktree_path}\0${normalized.branch}` === dedupeKey;
|
|
1201
|
+
});
|
|
1202
|
+
if (!alreadyPresent) {
|
|
1203
|
+
worktrees.push(recorded);
|
|
1204
|
+
}
|
|
1205
|
+
const manifestToWrite = `${JSON.stringify(writeBack, null, 2)}\n`;
|
|
1206
|
+
// 5. NOW run the git side effect. Every manifest problem above is caught
|
|
1207
|
+
// before this point, so a malformed/mis-shaped manifest can never leave
|
|
1208
|
+
// an unmanifested worktree on disk. `executeWorktreeCreatePlan` itself
|
|
1209
|
+
// best-effort rolls back a partial worktree on its own add-timeout /
|
|
1210
|
+
// add-failed paths (#2584 FIX 3).
|
|
1211
|
+
const result = executeWorktreeCreatePlan(plan, cwd, deps);
|
|
1212
|
+
if (!result.ok) {
|
|
1213
|
+
writeErr(`[gsd] worktree.create: ${result.reason} — ${result.stderr || ''}\n`);
|
|
1214
|
+
write(`${JSON.stringify({ ok: false, reason: result.reason, stderr: result.stderr }, null, 2)}\n`);
|
|
1215
|
+
process.exitCode = 1;
|
|
1216
|
+
return { ok: false, reason: result.reason, stderr: result.stderr };
|
|
1217
|
+
}
|
|
1218
|
+
// 6. Write the pre-computed manifest string, guarded. A write failure here
|
|
1219
|
+
// means a REAL worktree now exists with NO manifest entry — roll it back
|
|
1220
|
+
// (best-effort) rather than leaving an orphan cleanup-wave/reap-orphans
|
|
1221
|
+
// can never reach (#2584 FIX 1). Never throws past this function.
|
|
1222
|
+
const writeFile = deps.writeFile || ((p, content) => node_fs_1.default.writeFileSync(p, content, 'utf8'));
|
|
1223
|
+
try {
|
|
1224
|
+
writeFile(resolved, manifestToWrite);
|
|
1225
|
+
}
|
|
1226
|
+
catch (err) {
|
|
1227
|
+
const execGit = deps.execGit || execGitDefault;
|
|
1228
|
+
rollbackPartialWorktree(execGit, plan.entry.worktree_path, cwd);
|
|
1229
|
+
const hint = `The worktree was created but the manifest write failed (${err.message}); rolled back the worktree via a best-effort 'git worktree remove --force'.`;
|
|
1230
|
+
writeErr(`[gsd] worktree.create: manifest_write_failed — ${hint}\n`);
|
|
1231
|
+
write(`${JSON.stringify({ ok: false, reason: 'manifest_write_failed', hint, error: err.message }, null, 2)}\n`);
|
|
1232
|
+
process.exitCode = 1;
|
|
1233
|
+
return { ok: false, reason: 'manifest_write_failed', hint };
|
|
1234
|
+
}
|
|
1235
|
+
write(`${JSON.stringify({ ok: true, reason: 'created', entry: recorded, cwd: result.cwd, manifest_path: resolved }, null, 2)}\n`);
|
|
1236
|
+
return { ok: true, reason: 'created', entry: recorded, cwd: result.cwd, manifest_path: resolved };
|
|
1237
|
+
}
|
|
896
1238
|
/**
|
|
897
1239
|
* Reap orphaned linked worktrees whose lock owner process is dead, whose
|
|
898
1240
|
* branch tip is fully merged into the default branch, and whose lock file
|
|
@@ -1180,6 +1522,9 @@ module.exports = {
|
|
|
1180
1522
|
cmdWorktreeCleanupWave,
|
|
1181
1523
|
planWorktreeRecordAgent,
|
|
1182
1524
|
cmdWorktreeRecordAgent,
|
|
1525
|
+
planWorktreeCreate,
|
|
1526
|
+
executeWorktreeCreatePlan,
|
|
1527
|
+
cmdWorktreeCreate,
|
|
1183
1528
|
reapOrphanWorktrees,
|
|
1184
1529
|
cmdWorktreeReapOrphans,
|
|
1185
1530
|
resolveWorktreeRoot,
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"workflow.auto_advance",
|
|
20
20
|
"workflow.node_repair",
|
|
21
21
|
"workflow.node_repair_budget",
|
|
22
|
+
"workflow.smart_zone_tokens",
|
|
22
23
|
"workflow.human_verify_mode",
|
|
23
24
|
"workflow.text_mode",
|
|
24
25
|
"workflow.research_before_questions",
|
|
@@ -48,9 +49,6 @@
|
|
|
48
49
|
"planning.commit_docs",
|
|
49
50
|
"planning.search_gitignored",
|
|
50
51
|
"planning.sub_repos",
|
|
51
|
-
"review.ollama_host",
|
|
52
|
-
"review.lm_studio_host",
|
|
53
|
-
"review.llama_cpp_host",
|
|
54
52
|
"review.default_reviewers",
|
|
55
53
|
"review.max_prompt_tokens",
|
|
56
54
|
"review.max_prompt_tokens_per_reviewer",
|
|
@@ -72,6 +70,7 @@
|
|
|
72
70
|
"statusline.show_last_command",
|
|
73
71
|
"statusline.context_position",
|
|
74
72
|
"statusline.show_context_tokens",
|
|
73
|
+
"statusline.state_format",
|
|
75
74
|
"statusline.show_git",
|
|
76
75
|
"workflow.max_discuss_passes",
|
|
77
76
|
"features.thinking_partner",
|
|
@@ -116,11 +115,6 @@
|
|
|
116
115
|
"source": "^agent_skills\\.[a-zA-Z0-9_-]+$",
|
|
117
116
|
"description": "agent_skills.<agent-type>"
|
|
118
117
|
},
|
|
119
|
-
{
|
|
120
|
-
"topLevel": "review",
|
|
121
|
-
"source": "^review\\.models\\.[a-zA-Z0-9_-]+$",
|
|
122
|
-
"description": "review.models.<cli-name>"
|
|
123
|
-
},
|
|
124
118
|
{
|
|
125
119
|
"topLevel": "features",
|
|
126
120
|
"source": "^features\\.[a-zA-Z0-9_]+$",
|
|
@@ -148,8 +142,8 @@
|
|
|
148
142
|
},
|
|
149
143
|
{
|
|
150
144
|
"topLevel": "dynamic_routing",
|
|
151
|
-
"source": "^dynamic_routing\\.(enabled|escalate_on_failure|max_escalations|tier_models\\.(light|standard|heavy))$",
|
|
152
|
-
"description": "dynamic_routing.<enabled|escalate_on_failure|max_escalations|tier_models.<light|standard|heavy>>"
|
|
145
|
+
"source": "^dynamic_routing\\.(enabled|escalate_on_failure|max_escalations|provider_escalation|tier_models\\.(light|standard|heavy))$",
|
|
146
|
+
"description": "dynamic_routing.<enabled|escalate_on_failure|max_escalations|provider_escalation|tier_models.<light|standard|heavy>>"
|
|
153
147
|
},
|
|
154
148
|
{
|
|
155
149
|
"topLevel": "model_overrides",
|
|
@@ -176,11 +170,6 @@
|
|
|
176
170
|
"source": "^fast_mode\\.agent_overrides\\.[a-zA-Z0-9_-]+$",
|
|
177
171
|
"description": "fast_mode.agent_overrides.<agent-id>"
|
|
178
172
|
},
|
|
179
|
-
{
|
|
180
|
-
"topLevel": "review",
|
|
181
|
-
"source": "^review\\.max_prompt_tokens_per_reviewer\\.[a-zA-Z0-9_-]+$",
|
|
182
|
-
"description": "review.max_prompt_tokens_per_reviewer.<reviewer-slug>"
|
|
183
|
-
},
|
|
184
173
|
{
|
|
185
174
|
"topLevel": "review",
|
|
186
175
|
"source": "^review\\.reviewer_instances\\.[a-zA-Z0-9_-]+\\.(cli|model|agent)$",
|
|
@@ -24,13 +24,26 @@ treated as an external-API integration when **either**:
|
|
|
24
24
|
1. a `COVERAGE.md` matrix is present in the phase directory (the planner produced
|
|
25
25
|
one at `plan:pre`), **or**
|
|
26
26
|
2. the phase scope shows a strong external-API-integration signal (an integration
|
|
27
|
-
verb
|
|
28
|
-
API|SDK|REST|GraphQL` surface) and no matrix
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
verb and an external-API noun **in the same clause**, or an explicit
|
|
28
|
+
`<Service> API|SDK|REST|GraphQL` surface naming a real service) and no matrix
|
|
29
|
+
yet exists.
|
|
30
|
+
|
|
31
|
+
The detector is deliberately **fail-closed**: it leans toward firing, because a
|
|
32
|
+
false positive is dismissed by a one-line `COVERAGE.md` "no external API
|
|
33
|
+
integration" declaration, whereas a false *negative* silently lets a real
|
|
34
|
+
external-API phase past this blocking gate — strictly worse. So it suppresses
|
|
35
|
+
only prose that is unambiguously not external integration. A bare word like
|
|
36
|
+
"api" in "the public API of UserController" is ignored (no integration verb +
|
|
37
|
+
named service); the clause boundary is the whole relationship test, so an
|
|
38
|
+
integration verb and an API noun in **different** clauses do not pair. Since
|
|
39
|
+
#2365 the detector also excludes non-prose spans before matching: fenced code
|
|
40
|
+
blocks, inline `` `code` `` spans, and path-shaped tokens (a first-party
|
|
41
|
+
`src/app/api/profile/route.ts` route is a file path, not an external API, while
|
|
42
|
+
an external host like `api.stripe.com/v1` still counts). In the
|
|
43
|
+
`<Service> API` surface position it rejects capitalized sentence starters
|
|
44
|
+
("The API"), locality/protocol descriptors ("Internal API", "REST API"),
|
|
45
|
+
compound modifiers ("Resolver-only API"), and first-party-qualified services
|
|
46
|
+
("internal Payments API") — a real vendor name is none of these.
|
|
34
47
|
|
|
35
48
|
## The two touch points
|
|
36
49
|
|
|
@@ -70,6 +83,23 @@ must be non-empty and unique; every decision must be `INTEGRATE` or `OPT-OUT`;
|
|
|
70
83
|
every `OPT-OUT` must have a reason. Violations block the seal with a precise
|
|
71
84
|
error.
|
|
72
85
|
|
|
86
|
+
### Declaring "no external API integration" (#2365)
|
|
87
|
+
|
|
88
|
+
A phase that integrates no external API/SDK/service — but was still asked for a
|
|
89
|
+
matrix (e.g. the detector over-fired, or a team wants the decision on record) —
|
|
90
|
+
declares it instead of fabricating a row:
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
No external API integration: UI-only phase, no third-party surface.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The reason is **required**, exactly like an `OPT-OUT` reason — the declaration
|
|
97
|
+
is a reasoned decision, not a bypass. A `COVERAGE.md` containing both the
|
|
98
|
+
declaration and coverage rows is contradictory and blocks the seal. When the
|
|
99
|
+
detector still finds integration signals in the phase scope, the declaration
|
|
100
|
+
wins (it is the human overrule for a fallible detector) but the gate output
|
|
101
|
+
surfaces the overridden signals so the contradiction is visible, not silent.
|
|
102
|
+
|
|
73
103
|
## A second integration against the same need
|
|
74
104
|
|
|
75
105
|
A second platform for an existing capability (e.g. adding YouTube alongside
|
|
@@ -474,7 +474,7 @@ npm run dev &
|
|
|
474
474
|
DEV_SERVER_PID=$!
|
|
475
475
|
|
|
476
476
|
# Wait for ready (max 30s) — uses fetch() for cross-platform compatibility
|
|
477
|
-
timeout 30 bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done'
|
|
477
|
+
gsd_run run-with-timeout 30 -- bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done'
|
|
478
478
|
```
|
|
479
479
|
|
|
480
480
|
**Port conflicts:** Kill stale process (`lsof -ti:3000 | xargs kill`) or use alternate port (`--port 3001`).
|
|
@@ -97,6 +97,19 @@ Checklist of frequent bug patterns to scan before forming hypotheses. Ordered by
|
|
|
97
97
|
3. **Each checked pattern is a hypothesis candidate** — verify or eliminate with evidence
|
|
98
98
|
4. **If no pattern matches**, proceed to open-ended investigation
|
|
99
99
|
|
|
100
|
+
### Pattern categories → bug taxonomy (Phase 1.75)
|
|
101
|
+
|
|
102
|
+
The categories here feed bug-class classification (see `debugger-bug-taxonomy.md`):
|
|
103
|
+
|
|
104
|
+
| Pattern category | Typical bug_class |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Null / Undefined, Off-by-One, State, Import, Type, Regex, Error Handling, Scope | Bohrbug (deterministic) |
|
|
107
|
+
| Async / Timing (intermittent, leaked timer, init order) | Heisenbug / Concurrency |
|
|
108
|
+
| Environment / Config (works-here-not-there) | Heisenbug / Mandelbug (or config-as-root-cause) |
|
|
109
|
+
| Data Shape / API Contract | Bohrbug (or Mandelbug if volume-dependent) |
|
|
110
|
+
|
|
111
|
+
The taxonomy routes the investigation technique (SBFL + bisect for Bohrbugs; record-replay/stability for Heisenbugs; atomicity/order/deadlock checklist for Concurrency).
|
|
112
|
+
|
|
100
113
|
### Symptom-to-Category Quick Map
|
|
101
114
|
|
|
102
115
|
| Symptom | Check First |
|
|
@@ -83,3 +83,43 @@ Either list works — `enabledMcpjsonServers` is an explicit allow-list, `disabl
|
|
|
83
83
|
### Composition with model_profile
|
|
84
84
|
|
|
85
85
|
Trimming MCPs and tuning `model_profile` are independent levers that **compound**. Disabling a 25k-token MCP saves 25k per turn whether you're running `quality` (opus everywhere) or `budget` (sonnet/haiku); the savings are additive, not in lieu of model tuning. Don't pick one — do both, and audit MCPs first because the per-turn savings show up immediately and stack across every subagent the orchestrator spawns.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Phase Sizing (gsd-planner)
|
|
90
|
+
|
|
91
|
+
## Estimate Emission (#2631, ADR-2629)
|
|
92
|
+
|
|
93
|
+
Every plan carries an `estimate` block. It is the quantitative reason a phase must be sliced — tracer-first says *slice thin*, the estimate says *how thin, for this codebase*.
|
|
94
|
+
|
|
95
|
+
**Compute it:**
|
|
96
|
+
1. Sum `estimateTokens`-scale cost across the plan: implementation + the files each task reads + verification output. Roughly chars/4 over what the executor will actually touch.
|
|
97
|
+
2. Run `estimate-calibration` and **multiply your raw figure by its `factor`.** It is the measured estimate-vs-actual ratio for THIS project — a factor of 1 means there is not yet enough history to correct.
|
|
98
|
+
3. `confidence` is **derived, not judged**: it is the `confidence` value from the same calibration query, keyed to the sample count (`low` <3, `med` 3–5, `high` ≥6). **Do not rate your own certainty.** Self-rated confidence was measured in this project and found weak (`references/honest-verifier.md:25-29`); every signal here routes on measured history instead.
|
|
99
|
+
|
|
100
|
+
**Over budget?** The plan-checker flags a plan whose estimate exceeds `workflow.smart_zone_tokens`. This is advisory — it never blocks. When flagged, re-slice: a tracer plus expansion slices, each inside the budget. Prefer more, smaller plans over one that spends the agent's best early-context tokens and finishes degraded.
|
|
101
|
+
|
|
102
|
+
## Context Budget Rules
|
|
103
|
+
|
|
104
|
+
Plans should complete within ~50% context (not 80%). No context anxiety, quality maintained start to finish, room for unexpected complexity.
|
|
105
|
+
|
|
106
|
+
**Each plan: 2-3 tasks maximum.**
|
|
107
|
+
|
|
108
|
+
| Context Weight | Tasks/Plan | Context/Task | Total |
|
|
109
|
+
|----------------|------------|--------------|-------|
|
|
110
|
+
| Light (CRUD, config) | 3 | ~10-15% | ~30-45% |
|
|
111
|
+
| Medium (auth, payments) | 2 | ~20-30% | ~40-50% |
|
|
112
|
+
| Heavy (migrations, multi-subsystem) | 1-2 | ~30-40% | ~30-50% |
|
|
113
|
+
|
|
114
|
+
## Split Signals
|
|
115
|
+
|
|
116
|
+
**ALWAYS split if:**
|
|
117
|
+
- More than 3 tasks
|
|
118
|
+
- Multiple subsystems (DB + API + UI = separate plans)
|
|
119
|
+
- Any task with >5 file modifications
|
|
120
|
+
- Checkpoint + implementation in same plan
|
|
121
|
+
- Discovery + implementation in same plan
|
|
122
|
+
|
|
123
|
+
**CONSIDER splitting:** >5 files total, natural semantic boundaries, context cost estimate exceeds 40% for a single plan. See `<planner_authority_limits>` for prohibited split reasons.
|
|
124
|
+
|
|
125
|
+
See @~/.claude/gsd-core/references/planner-guidance.md for Granularity Calibration table (Coarse/Standard/Fine plans-per-phase).
|