@opengsd/gsd-core 1.16.0 → 1.18.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 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +398 -95
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +34 -12
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +138 -80
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +8 -5
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +33 -9
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +43 -16
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +141 -11
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +100 -53
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +98 -19
- package/gsd-core/bin/lib/state.cjs +544 -126
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +21 -2
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +40 -7
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +19 -11
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +189 -15
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +189 -15
- package/hooks/gsd-statusline.js +321 -22
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +15 -7
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -39,6 +39,27 @@ cleanup_temp_files() {
|
|
|
39
39
|
}
|
|
40
40
|
trap cleanup_temp_files EXIT
|
|
41
41
|
|
|
42
|
+
# The first line of $1 (everything before the first newline), matched BYTE-wise.
|
|
43
|
+
#
|
|
44
|
+
# `${1%%$'\n'*}` is a longest-suffix glob match, and on bash 3.2 (macOS's
|
|
45
|
+
# /bin/bash) in a multibyte locale such as en_US.UTF-8 it costs ~3.5 s on a
|
|
46
|
+
# 112KB value (#5473, measured inside the macOS conformance job: the same hook
|
|
47
|
+
# on the same 114750-byte command took ~5.6 s under LC_ALL=en_US.UTF-8 and
|
|
48
|
+
# ~0.55 s under LC_ALL=C, and a trace put 3.5 s of it on this expansion). A
|
|
49
|
+
# newline is the same single byte in every locale, so matching it byte-wise
|
|
50
|
+
# changes no result.
|
|
51
|
+
#
|
|
52
|
+
# ONLY call this inside a command substitution, `SUBJECT=$(gsd_first_line "$MSG")`:
|
|
53
|
+
# the substitution's subshell is what keeps LC_ALL=C from leaking into the rest
|
|
54
|
+
# of the hook, whose regexes and length arithmetic keep the caller's locale. A
|
|
55
|
+
# substitution is a fork with no pipe between two processes, so it keeps the
|
|
56
|
+
# property the pure-expansion form was chosen for (no `head -1` SIGPIPE race
|
|
57
|
+
# under `set -euo pipefail`, #4429); printf is a builtin, so no exec either.
|
|
58
|
+
gsd_first_line() {
|
|
59
|
+
LC_ALL=C
|
|
60
|
+
printf '%s' "${1%%$'\n'*}"
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
# The 10 built-in Conventional Commits types — the SINGLE declaration (#3811
|
|
43
64
|
# review finding: this was previously hand-typed a second time inside the
|
|
44
65
|
# node -e script below, a generative-fix-divergence risk per CLAUDE.md's
|
|
@@ -575,18 +596,19 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
575
596
|
SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
|
|
576
597
|
const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
|
|
577
598
|
process.stdout.write(resolveCommitSubject(process.env.MSG));
|
|
578
|
-
" 2>/dev/null) || SUBJECT
|
|
599
|
+
" 2>/dev/null) || SUBJECT=$(gsd_first_line "$MSG")
|
|
579
600
|
else
|
|
580
|
-
# Pure parameter expansion, not
|
|
601
|
+
# Pure parameter expansion (in gsd_first_line), not
|
|
602
|
+
# `echo "$MSG" | head -1`: that pipeline
|
|
581
603
|
# raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
|
|
582
604
|
# (the common case) — `head -1` can close its read end as soon as it
|
|
583
605
|
# has the first line, and if `echo`'s write lands after that close,
|
|
584
606
|
# `echo` dies with signal 13 (exit 141), which is NOT suppressed by
|
|
585
607
|
# `set -e` and aborted the whole hook intermittently (observed in
|
|
586
|
-
# tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case).
|
|
587
|
-
#
|
|
608
|
+
# tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). No pipe
|
|
609
|
+
# means no race surface. Equivalent to
|
|
588
610
|
# `head -1` for single-line, multi-line, and trailing-newline input.
|
|
589
|
-
SUBJECT
|
|
611
|
+
SUBJECT=$(gsd_first_line "$MSG")
|
|
590
612
|
fi
|
|
591
613
|
# Single source of truth for the accepted commit-type list (#3811): the
|
|
592
614
|
# 10 built-ins plus whatever passed the safe-token filter above. Both the
|
|
@@ -227,7 +227,9 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
|
|
|
227
227
|
// never a resolution failure (this function only ever throws for the
|
|
228
228
|
// config.json read above, which the caller's catch already handles).
|
|
229
229
|
try {
|
|
230
|
-
|
|
230
|
+
// #5017: the store root is GSD_HOME when set and non-blank, else the home
|
|
231
|
+
// directory — the config loader's rule (src/runtime-homes.cts resolveGsdHome).
|
|
232
|
+
const defaultsPath = path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json');
|
|
231
233
|
const defaultsRaw = fs.readFileSync(defaultsPath, 'utf-8');
|
|
232
234
|
const defaultsParsed = JSON.parse(defaultsRaw);
|
|
233
235
|
if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
|
|
@@ -67,26 +67,32 @@ const cacheFile = path.join(cacheDir, updateCacheFileName);
|
|
|
67
67
|
const projectVersionFile = path.join(projectConfigDir, 'gsd-core', 'VERSION');
|
|
68
68
|
const globalVersionFile = path.join(globalConfigDir, 'gsd-core', 'VERSION');
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
|
|
70
|
+
// This background check is advisory. Cache or worker failures must not make
|
|
71
|
+
// the SessionStart hook fail. Recursive mkdir is safe if another session
|
|
72
|
+
// creates the directory concurrently.
|
|
73
|
+
try {
|
|
72
74
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
73
|
-
}
|
|
74
75
|
|
|
75
|
-
// Run check in background via a dedicated worker script.
|
|
76
|
-
// Spawning a file (rather than node -e '<inline code>') keeps the worker logic
|
|
77
|
-
// in plain JS with no template-literal regex-escaping concerns, and makes the
|
|
78
|
-
// worker independently testable.
|
|
79
|
-
const workerPath = path.join(__dirname, 'gsd-check-update-worker.js');
|
|
80
|
-
const child = spawn(process.execPath, [workerPath], {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
76
|
+
// Run check in background via a dedicated worker script.
|
|
77
|
+
// Spawning a file (rather than node -e '<inline code>') keeps the worker logic
|
|
78
|
+
// in plain JS with no template-literal regex-escaping concerns, and makes the
|
|
79
|
+
// worker independently testable.
|
|
80
|
+
const workerPath = path.join(__dirname, 'gsd-check-update-worker.js');
|
|
81
|
+
const child = spawn(process.execPath, [workerPath], {
|
|
82
|
+
stdio: 'ignore',
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
detached: true, // Required on Windows for proper process detachment
|
|
85
|
+
env: {
|
|
86
|
+
...process.env,
|
|
87
|
+
GSD_CACHE_FILE: cacheFile,
|
|
88
|
+
GSD_PROJECT_VERSION_FILE: projectVersionFile,
|
|
89
|
+
GSD_GLOBAL_VERSION_FILE: globalVersionFile,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
91
92
|
|
|
92
|
-
child.
|
|
93
|
+
// A spawn failure can also arrive asynchronously on the child process.
|
|
94
|
+
child.on('error', () => {});
|
|
95
|
+
child.unref();
|
|
96
|
+
} catch {
|
|
97
|
+
// Update hints are best effort; a failed check must not interrupt startup.
|
|
98
|
+
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// Thresholds:
|
|
15
15
|
// WARNING (remaining <= 35%): Agent should wrap up current task
|
|
16
16
|
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
|
17
|
+
// "remaining" is of the auto-compact threshold — the statusline puts the
|
|
18
|
+
// bridge on that scale (#4985), so both fire before Claude Code compacts.
|
|
17
19
|
// Both fire-points are overridable per project via .planning/config.json
|
|
18
20
|
// (hooks.context_warning_threshold / hooks.context_critical_threshold, #4285);
|
|
19
21
|
// the values above are the defaults used when the keys are absent or unusable.
|
|
@@ -500,23 +502,30 @@ const handleStdinEnd = () => {
|
|
|
500
502
|
}
|
|
501
503
|
|
|
502
504
|
// Build advisory warning message (never use imperative commands that
|
|
503
|
-
// override user preferences — see #884)
|
|
505
|
+
// override user preferences — see #884). The percentages are of the
|
|
506
|
+
// auto-compact threshold (#4985); the token counts, when the bridge has
|
|
507
|
+
// them, are the ones /context shows (#2451).
|
|
508
|
+
const kTokens = n => `${Math.round(n / 1000)}k`;
|
|
509
|
+
const tokenDetail = Number.isFinite(metrics.used_tokens) && Number.isFinite(metrics.threshold_tokens)
|
|
510
|
+
? ` (${kTokens(metrics.used_tokens)} of ${kTokens(metrics.threshold_tokens)} usable tokens)`
|
|
511
|
+
: '';
|
|
512
|
+
const usage = `Usage at ${usedPct}%${tokenDetail}. Remaining: ${remaining}%. `;
|
|
504
513
|
let message;
|
|
505
514
|
if (isCritical) {
|
|
506
515
|
message = isGsdActive
|
|
507
|
-
? `CONTEXT CRITICAL:
|
|
516
|
+
? `CONTEXT CRITICAL: ${usage}` +
|
|
508
517
|
'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
|
|
509
518
|
'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
|
|
510
519
|
'/gsd:pause-work at the next natural stopping point.'
|
|
511
|
-
: `CONTEXT CRITICAL:
|
|
520
|
+
: `CONTEXT CRITICAL: ${usage}` +
|
|
512
521
|
'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
|
|
513
522
|
'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
|
|
514
523
|
} else {
|
|
515
524
|
message = isGsdActive
|
|
516
|
-
? `CONTEXT WARNING:
|
|
525
|
+
? `CONTEXT WARNING: ${usage}` +
|
|
517
526
|
'Context is getting limited. Avoid starting new complex work. If not between ' +
|
|
518
527
|
'defined plan steps, inform the user so they can prepare to pause.'
|
|
519
|
-
: `CONTEXT WARNING:
|
|
528
|
+
: `CONTEXT WARNING: ${usage}` +
|
|
520
529
|
'Be aware that context is getting limited. Avoid unnecessary exploration or ' +
|
|
521
530
|
'starting new complex work.';
|
|
522
531
|
}
|
|
@@ -435,7 +435,8 @@ function resolveFallbackIsolation(root, configPath) {
|
|
|
435
435
|
}
|
|
436
436
|
if (!runtimeId) {
|
|
437
437
|
try {
|
|
438
|
-
|
|
438
|
+
// #5017: GSD_HOME when set and non-blank, else the home directory — the loader's rule.
|
|
439
|
+
const defaultsPath = path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json');
|
|
439
440
|
const defaultsParsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
|
|
440
441
|
if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
|
|
441
442
|
runtimeId = resolveRuntimeNameFromCandidates(defaultsParsed.runtime) || null;
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
// `git check-ignore` / `git ls-files` / `git rm --cached`, and the
|
|
68
68
|
// destination of a one-source `cp`/`mv` (#4856). The #4856 carve-outs
|
|
69
69
|
// fail closed: an option they do not list withdraws them from the segment.
|
|
70
|
+
// A git global option the walker does not know also keeps the #5045 `-c`
|
|
71
|
+
// scan running over every later word (see walkGitGlobals).
|
|
70
72
|
// A shell interpreter (bash/sh/zsh/dash/ksh/su) has its script scanned
|
|
71
73
|
// whether it arrives via `-c '…'`, a `<( )` file operand, a heredoc /
|
|
72
74
|
// here-string, or a pipe from a knowable `echo`/`printf` source
|
|
@@ -103,9 +105,23 @@
|
|
|
103
105
|
// edit of `.env` lands in the link target — a write redirection, the
|
|
104
106
|
// out-of-scope write class, not a read.)
|
|
105
107
|
//
|
|
108
|
+
// A second, unrelated contract is enforced on the Bash leg because this is the
|
|
109
|
+
// one hook that already parses EVERY Bash command (#5214): a Bash command that
|
|
110
|
+
// launches the debugger as a CLI process — `claude … --agent gsd-debugger` — is
|
|
111
|
+
// denied (`debugger-cli-spawn`). The debug session manager is required to spawn
|
|
112
|
+
// gsd-debugger with `Agent(run_in_background=false)` so it can read the
|
|
113
|
+
// debugger's structured return; on a captured 1.15.0 run it routed around that
|
|
114
|
+
// literal and shelled out with the Bash parameter `run_in_background: true`,
|
|
115
|
+
// so no return ever came back and the checkpoint never advanced. Prose in the
|
|
116
|
+
// agent file cannot stop an improvised tool call; a PreToolUse denial can, and
|
|
117
|
+
// Claude Code runs plugin hooks for a subagent's tool calls too. A person
|
|
118
|
+
// running `claude --agent gsd-debugger` in their own terminal never reaches
|
|
119
|
+
// this hook. Only the debugger is named: it is the agent whose blocking return
|
|
120
|
+
// the managed debug loop depends on.
|
|
121
|
+
//
|
|
106
122
|
// Triggers on: Read, Grep, Bash tool calls (Kimi: ReadFile, Grep, Shell)
|
|
107
123
|
// Action: BLOCK (decision: 'block', exit 2) — codes secret-read |
|
|
108
|
-
// glob-too-complex | command-too-large
|
|
124
|
+
// glob-too-complex | command-too-large | debugger-cli-spawn
|
|
109
125
|
// No-op: other tools, non-secret targets, hook errors (fail open — a parser
|
|
110
126
|
// bug in a hook that runs on EVERY Bash call must never brick a
|
|
111
127
|
// session; the crash policy is declared once below).
|
|
@@ -178,14 +194,29 @@ const GIT_PATHSPEC_SUBCOMMANDS = new Map([
|
|
|
178
194
|
}],
|
|
179
195
|
]);
|
|
180
196
|
|
|
181
|
-
// git's global options (
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
197
|
+
// git's global options (git.c handle_options, git 2.49) — the ONE vocabulary
|
|
198
|
+
// both the #4856 pathspec exemption and the #5045 -c scan walk, via
|
|
199
|
+
// walkGitGlobals. A value option consumes the next word; the long options in
|
|
200
|
+
// GIT_GLOBAL_EQUALS_FORM_OPTIONS also take it attached as `--opt=value`.
|
|
201
|
+
// `-C`, `-c` and the hidden `--shallow-file` accept only the separate word
|
|
202
|
+
// (git rejects `--shallow-file=x` and `-c<name>=<value>`), and
|
|
203
|
+
// `--exec-path` runs a subcommand only as `--exec-path=<path>` (bare, it
|
|
204
|
+
// prints the path and exits). Any other option fails closed for both
|
|
205
|
+
// consumers: its arity is unknown, so its value could be read as the
|
|
206
|
+
// subcommand (`git -C ls-files show HEAD:.env` runs `show`). The pathspec
|
|
207
|
+
// exemption is withdrawn, and the -c scan keeps collecting every later
|
|
208
|
+
// `-c <value>` pair instead of stopping.
|
|
209
|
+
const GIT_GLOBAL_VALUE_OPTIONS = new Set([
|
|
210
|
+
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source',
|
|
211
|
+
'--shallow-file',
|
|
212
|
+
]);
|
|
213
|
+
const GIT_GLOBAL_EQUALS_FORM_OPTIONS = new Set([
|
|
214
|
+
'--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--exec-path',
|
|
215
|
+
]);
|
|
186
216
|
const GIT_GLOBAL_FLAGS = new Set([
|
|
187
217
|
'-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--no-lazy-fetch',
|
|
188
218
|
'--no-optional-locks', '--no-advice', '--bare',
|
|
219
|
+
'--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs',
|
|
189
220
|
]);
|
|
190
221
|
|
|
191
222
|
// #4856: `cp`/`mv` write their destination and never print it, so a secret
|
|
@@ -775,6 +806,29 @@ function resolveCommand(words) {
|
|
|
775
806
|
return { base: lastSegment(words[idx].text).toLowerCase(), operands: words.slice(idx + 1) };
|
|
776
807
|
}
|
|
777
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Rescan only the command-valued -c keys selected by #5045. This deliberately
|
|
811
|
+
* does not resolve config files, --config-env/GIT_CONFIG_* values, or other
|
|
812
|
+
* executing keys (diff drivers, filters, credential helpers, ssh, gpg).
|
|
813
|
+
* Those are documented gaps, not a promise of complete Git config analysis.
|
|
814
|
+
* Global option values and subcommand arguments are data, not more -c flags.
|
|
815
|
+
*/
|
|
816
|
+
function scanGitConfig(operands, depth) {
|
|
817
|
+
for (const config of walkGitGlobals(operands).configs) {
|
|
818
|
+
const eq = config.indexOf('=');
|
|
819
|
+
if (eq === -1) continue;
|
|
820
|
+
const key = config.slice(0, eq).toLowerCase();
|
|
821
|
+
const value = config.slice(eq + 1);
|
|
822
|
+
let script;
|
|
823
|
+
if (key.startsWith('alias.') && value.startsWith('!')) script = value.slice(1);
|
|
824
|
+
else if (key === 'diff.external') script = value;
|
|
825
|
+
else continue;
|
|
826
|
+
const hit = findSecretRead(script, depth + 1);
|
|
827
|
+
if (hit) return hit;
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
|
|
778
832
|
// Operand indices the segment's command consumes as a NAME, never as
|
|
779
833
|
// CONTENTS (#4639, #4856). Only these positions skip the operand check;
|
|
780
834
|
// every other operand is still checked, so a carve-out cannot launder a read
|
|
@@ -806,26 +860,38 @@ function isListedFlag(text, flags) {
|
|
|
806
860
|
return /^-[A-Za-z]+$/.test(text) && [...text.slice(1)].every((ch) => flags.has(`-${ch}`));
|
|
807
861
|
}
|
|
808
862
|
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
|
|
863
|
+
// Walks git's global options (see GIT_GLOBAL_VALUE_OPTIONS). `sub` is the
|
|
864
|
+
// subcommand's index, or -1 when an unlisted option (or the end of the words)
|
|
865
|
+
// comes first; `configs` holds every `-c` value seen on the way, in order.
|
|
866
|
+
// Past an unlisted option the subcommand cannot be located, so `configs`
|
|
867
|
+
// conservatively takes the word after every later `-c` as well: a read
|
|
868
|
+
// still needs a command-valued key, so a harmless value stays allowed.
|
|
869
|
+
function walkGitGlobals(operands) {
|
|
870
|
+
const configs = [];
|
|
812
871
|
for (let k = 0; k < operands.length; k++) {
|
|
813
872
|
const t = operands[k].text;
|
|
814
|
-
if (!t.startsWith('-')) return k;
|
|
815
|
-
if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
|
|
873
|
+
if (!t.startsWith('-')) return { sub: k, configs };
|
|
874
|
+
if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
|
|
875
|
+
if (t === '-c' && k + 1 < operands.length) configs.push(operands[k + 1].text);
|
|
876
|
+
k++;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
816
879
|
if (GIT_GLOBAL_FLAGS.has(t)) continue;
|
|
817
880
|
const eq = t.indexOf('=');
|
|
818
|
-
|
|
819
|
-
|
|
881
|
+
if (t.startsWith('--') && eq !== -1 && GIT_GLOBAL_EQUALS_FORM_OPTIONS.has(t.slice(0, eq))) continue;
|
|
882
|
+
for (let j = k + 1; j + 1 < operands.length; j++) {
|
|
883
|
+
if (operands[j].text === '-c') configs.push(operands[++j].text);
|
|
884
|
+
}
|
|
885
|
+
return { sub: -1, configs };
|
|
820
886
|
}
|
|
821
|
-
return -1;
|
|
887
|
+
return { sub: -1, configs };
|
|
822
888
|
}
|
|
823
889
|
|
|
824
890
|
// #4856: the pathspec operands of a GIT_PATHSPEC_SUBCOMMANDS invocation. git
|
|
825
891
|
// permutes options, so one may follow a pathspec; after `--` every word is a
|
|
826
892
|
// pathspec. The subcommand is matched case-sensitively, as git looks it up.
|
|
827
893
|
function gitPathspecIndices(operands) {
|
|
828
|
-
const sub =
|
|
894
|
+
const { sub } = walkGitGlobals(operands);
|
|
829
895
|
const spec = sub === -1 ? undefined : GIT_PATHSPEC_SUBCOMMANDS.get(operands[sub].text);
|
|
830
896
|
if (!spec) return new Set();
|
|
831
897
|
const pathspecs = new Set();
|
|
@@ -993,6 +1059,12 @@ function findSecretRead(command, depth) {
|
|
|
993
1059
|
// `bash .env` (file mode) is caught by the operand check below.
|
|
994
1060
|
}
|
|
995
1061
|
|
|
1062
|
+
// Git -c can carry a shell script instead of a file-name operand (#5045).
|
|
1063
|
+
if ((base === 'git' || base === 'git.exe') && depth < MAX_NESTING_DEPTH) {
|
|
1064
|
+
const hit = scanGitConfig(operands, depth);
|
|
1065
|
+
if (hit) return hit;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
996
1068
|
if (NON_READING_COMMANDS.has(base)) continue;
|
|
997
1069
|
|
|
998
1070
|
const nameOnly = nameOnlyOperandIndices(base, operands);
|
|
@@ -1004,6 +1076,100 @@ function findSecretRead(command, depth) {
|
|
|
1004
1076
|
return null;
|
|
1005
1077
|
}
|
|
1006
1078
|
|
|
1079
|
+
// ---------------------------------------------------------------------------
|
|
1080
|
+
// gsd-debugger CLI spawn scan (#5214)
|
|
1081
|
+
// ---------------------------------------------------------------------------
|
|
1082
|
+
|
|
1083
|
+
const DEBUGGER_AGENT_NAME = 'gsd-debugger';
|
|
1084
|
+
const CLAUDE_BINARY_RE = /^claude(\.exe)?$/;
|
|
1085
|
+
|
|
1086
|
+
// `gsd-debugger`, or a plugin-namespaced `<plugin>:gsd-debugger`.
|
|
1087
|
+
function isDebuggerAgent(value) {
|
|
1088
|
+
return value.slice(value.lastIndexOf(':') + 1) === DEBUGGER_AGENT_NAME;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// `timeout [flags] DURATION cmd …` is not in CMD_PREFIXES because its first
|
|
1092
|
+
// operand is a duration, not the command. Returns the command that follows it.
|
|
1093
|
+
function unwrapTimeout(cmd) {
|
|
1094
|
+
let { base, operands } = cmd;
|
|
1095
|
+
while (base === 'timeout') {
|
|
1096
|
+
let k = 0;
|
|
1097
|
+
while (k < operands.length && operands[k].text.startsWith('-')) k++;
|
|
1098
|
+
k++; // the DURATION operand
|
|
1099
|
+
const rest = resolveCommand(operands.slice(k));
|
|
1100
|
+
if (!rest) return null;
|
|
1101
|
+
({ base, operands } = rest);
|
|
1102
|
+
}
|
|
1103
|
+
return { base, operands };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Returns the `--agent` value text of a debugger launch, or null.
|
|
1107
|
+
function findDebuggerCliSpawn(command, depth) {
|
|
1108
|
+
const { tokens, nested } = tokenize(command);
|
|
1109
|
+
|
|
1110
|
+
for (const sub of nested) {
|
|
1111
|
+
if (depth < MAX_NESTING_DEPTH) {
|
|
1112
|
+
const hit = findDebuggerCliSpawn(sub, depth + 1);
|
|
1113
|
+
if (hit) return hit;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const bySeg = new Map();
|
|
1118
|
+
for (const t of tokens) {
|
|
1119
|
+
if (t.kind !== 'word' && t.kind !== 'op') continue;
|
|
1120
|
+
if (!bySeg.has(t.seg)) bySeg.set(t.seg, []);
|
|
1121
|
+
bySeg.get(t.seg).push(t);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
for (const segTokens of bySeg.values()) {
|
|
1125
|
+
const words = [];
|
|
1126
|
+
for (let k = 0; k < segTokens.length; k++) {
|
|
1127
|
+
const t = segTokens[k];
|
|
1128
|
+
if (t.kind === 'op') {
|
|
1129
|
+
// A redirect consumes its target word; it is never the command.
|
|
1130
|
+
if (!t.dup && segTokens[k + 1] && segTokens[k + 1].kind === 'word') k++;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
words.push(t);
|
|
1134
|
+
}
|
|
1135
|
+
if (!words.length) continue;
|
|
1136
|
+
|
|
1137
|
+
const first = resolveCommand(words);
|
|
1138
|
+
const cmd = first && unwrapTimeout(first);
|
|
1139
|
+
if (!cmd) continue;
|
|
1140
|
+
const { base, operands } = cmd;
|
|
1141
|
+
|
|
1142
|
+
if (base === 'eval') {
|
|
1143
|
+
if (depth < MAX_NESTING_DEPTH) {
|
|
1144
|
+
const hit = findDebuggerCliSpawn(operands.map((w) => w.text).join(' '), depth + 1);
|
|
1145
|
+
if (hit) return hit;
|
|
1146
|
+
}
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (SHELL_INTERPRETERS.has(base)) {
|
|
1151
|
+
const cIdx = operands.findIndex((w) => DASH_C_RE.test(w.text));
|
|
1152
|
+
if (cIdx !== -1 && operands[cIdx + 1] && depth < MAX_NESTING_DEPTH) {
|
|
1153
|
+
const hit = findDebuggerCliSpawn(operands[cIdx + 1].text, depth + 1);
|
|
1154
|
+
if (hit) return hit;
|
|
1155
|
+
}
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (!CLAUDE_BINARY_RE.test(base)) continue;
|
|
1160
|
+
for (let k = 0; k < operands.length; k++) {
|
|
1161
|
+
const text = operands[k].text;
|
|
1162
|
+
if (text === '--agent') {
|
|
1163
|
+
const value = operands[k + 1];
|
|
1164
|
+
if (value && isDebuggerAgent(value.text)) return value.text;
|
|
1165
|
+
} else if (text.startsWith('--agent=') && isDebuggerAgent(text.slice('--agent='.length))) {
|
|
1166
|
+
return text.slice('--agent='.length);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1007
1173
|
// A shell interpreter's script comes from `-c`, a file operand, or stdin.
|
|
1008
1174
|
function scanShellInterpreter(operands, heredocs, hereStrings, s, bySeg, sepAfter, depth) {
|
|
1009
1175
|
const cIdx = operands.findIndex((w) => DASH_C_RE.test(w.text));
|
|
@@ -1102,6 +1268,12 @@ function reasonFor(code, tool, target) {
|
|
|
1102
1268
|
return `Secret read guard: the Grep glob '${target}' expands to more than ${MAX_GLOB_ALTERNATIVES} ` +
|
|
1103
1269
|
'alternatives and cannot be checked for secret-file matches. Use a narrower glob.';
|
|
1104
1270
|
}
|
|
1271
|
+
if (code === 'debugger-cli-spawn') {
|
|
1272
|
+
return `Debugger spawn guard: this Bash command launches the debugger as a CLI process ('--agent ${target}'). ` +
|
|
1273
|
+
'A shell-spawned debugger runs detached, returns no structured result, and the debug checkpoint never advances. ' +
|
|
1274
|
+
'Spawn it with the Agent tool instead: Agent(subagent_type="gsd-debugger", run_in_background=false, ...) ' +
|
|
1275
|
+
'and wait for its structured return.';
|
|
1276
|
+
}
|
|
1105
1277
|
return `Secret read guard: ${tool} would read '${target}', which matches a protected secret-file ` +
|
|
1106
1278
|
`pattern (${PATTERN_TEXT}). Secret values must not be read into the conversation. ` +
|
|
1107
1279
|
'If you need a specific value, ask the user for it; if you need the variable NAMES, ' +
|
|
@@ -1253,6 +1425,8 @@ process.stdin.on('end', () => {
|
|
|
1253
1425
|
if (command.length > MAX_COMMAND_LENGTH) emitBlock('command-too-large', tool, '');
|
|
1254
1426
|
const hit = findSecretRead(command, 0);
|
|
1255
1427
|
if (hit !== null) emitBlock('secret-read', tool, hit);
|
|
1428
|
+
const spawn = findDebuggerCliSpawn(command, 0);
|
|
1429
|
+
if (spawn !== null) emitBlock('debugger-cli-spawn', tool, spawn);
|
|
1256
1430
|
allow(undefined);
|
|
1257
1431
|
} catch {
|
|
1258
1432
|
// Fail open — never block valid tool calls due to hook errors.
|