@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
|
@@ -37,6 +37,100 @@ function nearestExistingDir(start) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// #2304: Kimi's native hook bus delivers Kimi's tool vocabulary in the payload
|
|
41
|
+
// (Write → WriteFile, Edit/MultiEdit → StrReplaceFile) while the [[hooks]]
|
|
42
|
+
// matcher is registered pre-translated (runtime-hooks-surface.cts
|
|
43
|
+
// buildKimiHooksTomlBlock) — so without normalizing the payload too, the
|
|
44
|
+
// matcher fires but the tool_name check below exits 0 and the guard is dormant
|
|
45
|
+
// on Kimi. The tool_input field names differ as well (kimi-cli
|
|
46
|
+
// src/kimi_cli/tools/file/{write,replace}.py): WriteFile takes `path`/`content`,
|
|
47
|
+
// StrReplaceFile takes `path` + `edit: Edit | list[Edit]` with `old`/`new` —
|
|
48
|
+
// kimi-cli's hooks/events.py forwards tool_input verbatim, so both layers need
|
|
49
|
+
// mapping. Accepts bare and module-qualified ('kimi_cli.tools.file:WriteFile')
|
|
50
|
+
// names; unknown names fall through untouched. Inlined per guard (not
|
|
51
|
+
// hooks/lib/): hook scripts are staged as standalone files, and a sibling
|
|
52
|
+
// require is a staging dependency that can fail silently.
|
|
53
|
+
// A Map, not an object literal: bare bracket lookup resolves prototype keys
|
|
54
|
+
// ('constructor', '__proto__', 'toString') to truthy functions/objects, so the
|
|
55
|
+
// !mapped fall-through never fires for them; Map.get returns undefined (same
|
|
56
|
+
// shape as canonicalizeRuntimeName in src/runtime-name-policy.cts).
|
|
57
|
+
const KIMI_TOOL_NAMES = new Map([['WriteFile', 'Write'], ['StrReplaceFile', 'Edit'], ['ReadFile', 'Read'], ['Shell', 'Bash']]);
|
|
58
|
+
function normalizeKimiPayload(data) {
|
|
59
|
+
// #2595 (review nit): `JSON.parse('null')` is null, and null/primitive
|
|
60
|
+
// payloads reached the `data.tool_name` read below and threw — falsifying
|
|
61
|
+
// this function's own "total over the inputs JSON can express" claim, which
|
|
62
|
+
// property (e) now tests directly. Harmless in practice (a null payload has
|
|
63
|
+
// nothing to guard, and the throw landed in the same fail-open catch as the
|
|
64
|
+
// exit-0 it now takes deliberately) but the claim should be true as stated.
|
|
65
|
+
if (data === null || typeof data !== 'object') return data;
|
|
66
|
+
const raw = data.tool_name;
|
|
67
|
+
if (typeof raw !== 'string') return data;
|
|
68
|
+
const mapped = KIMI_TOOL_NAMES.get(raw.slice(raw.lastIndexOf(':') + 1));
|
|
69
|
+
if (!mapped) return data;
|
|
70
|
+
data.tool_name = mapped;
|
|
71
|
+
if (data.tool_response === undefined && data.tool_output !== undefined) {
|
|
72
|
+
data.tool_response = data.tool_output;
|
|
73
|
+
}
|
|
74
|
+
const input = data.tool_input;
|
|
75
|
+
if (input && typeof input === 'object') {
|
|
76
|
+
// #2547 (review): Kimi's `path` is AUTHORITATIVE — it must win outright,
|
|
77
|
+
// not merely fill in when `file_path` happens to be absent. kimi-cli's file
|
|
78
|
+
// tools carry no `file_path` field at all (src/kimi_cli/tools/file/write.py,
|
|
79
|
+
// replace.py, @ 4a550ef — the SHA #2547 pins), and soul/toolset.py hands the
|
|
80
|
+
// model's raw json-parsed
|
|
81
|
+
// arguments to PreToolUse verbatim, doing typed validation only later inside
|
|
82
|
+
// tool.call() — after the hook has already decided. So a `file_path` in a
|
|
83
|
+
// Kimi payload is ALWAYS model-supplied, and under the old `=== undefined`
|
|
84
|
+
// condition it SHADOWED the field kimi-cli actually executes on. A payload
|
|
85
|
+
// pairing a cross-root `path` with a spurious `file_path: ""` left every
|
|
86
|
+
// guard reading an empty string and exiting 0, while the identical write
|
|
87
|
+
// without the extra key blocked — a bypass needing no crash at all. The same
|
|
88
|
+
// shadowing also preserved a NON-STRING `file_path` (`[]`), which threw
|
|
89
|
+
// inside gsd-worktree-path-guard's path.isAbsolute() and reached its outer
|
|
90
|
+
// `catch { process.exit(0) }`: the same crash-to-allow this fix closes
|
|
91
|
+
// elsewhere, reached through the guard's own read rather than through
|
|
92
|
+
// normalization. Overwriting can only ever narrow what a guard inspects to
|
|
93
|
+
// the path that will actually be written, so it cannot under-block.
|
|
94
|
+
if (typeof input.path === 'string') {
|
|
95
|
+
input.file_path = input.path;
|
|
96
|
+
}
|
|
97
|
+
const edits = Array.isArray(input.edit) ? input.edit
|
|
98
|
+
: (input.edit && typeof input.edit === 'object') ? [input.edit] : [];
|
|
99
|
+
if (edits.length) {
|
|
100
|
+
// #2547: `e?.old`, not `e.old` — `??` guards the value, not the
|
|
101
|
+
// dereference, so a NULLISH entry (`edit: [null]`) threw a TypeError
|
|
102
|
+
// here. normalizeKimiPayload runs before any tool dispatch, so that throw
|
|
103
|
+
// reached each guard's outer `catch { process.exit(0) }` and silently
|
|
104
|
+
// downgraded a should-BLOCK call into an allow. (A string/number entry
|
|
105
|
+
// never threw — `('x').old` is a legal read yielding undefined.)
|
|
106
|
+
//
|
|
107
|
+
// The String() coercion is guarded for the same reason: `{"toString":
|
|
108
|
+
// null}` is valid JSON that throws "Cannot convert object to primitive
|
|
109
|
+
// value", which is the identical crash-to-allow with a different
|
|
110
|
+
// trigger. Degrading only the non-coercible entry to '' keeps
|
|
111
|
+
// stringification intact for every value that CAN coerce (numbers,
|
|
112
|
+
// arrays, plain objects), so nothing downstream — including
|
|
113
|
+
// gsd-prompt-guard's scan of new_string — loses content it saw before.
|
|
114
|
+
const editText = (v) => { try { return String(v ?? ''); } catch { return ''; } };
|
|
115
|
+
// #2595 (review Major 2): reconstruct UNCONDITIONALLY, mirroring the
|
|
116
|
+
// `path` decision above rather than merely filling in when the field
|
|
117
|
+
// happens to be absent. kimi-cli's StrReplaceFile schema is `path` +
|
|
118
|
+
// `edit` only (src/kimi_cli/tools/file/replace.py @ 4a550ef) — it carries
|
|
119
|
+
// no `old_string`/`new_string` at all, so either field appearing in a
|
|
120
|
+
// Kimi payload is ALWAYS model-supplied, exactly like `file_path`. Under
|
|
121
|
+
// the old `=== undefined` condition a model-supplied `new_string: ""`
|
|
122
|
+
// SHADOWED the reconstruction, leaving gsd-prompt-guard's injection scan
|
|
123
|
+
// reading '' and exiting at its `if (!content)` before it ever saw the
|
|
124
|
+
// real `edit[].new` — a one-key bypass of the very scan this fix's
|
|
125
|
+
// guarded coercion exists to keep fed. A `typeof` test would NOT close
|
|
126
|
+
// it: a benign non-empty string shadows just as effectively as ''.
|
|
127
|
+
input.old_string = edits.map((e) => editText(e?.old)).join('\n');
|
|
128
|
+
input.new_string = edits.map((e) => editText(e?.new)).join('\n');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
|
|
40
134
|
let input = '';
|
|
41
135
|
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
42
136
|
process.stdin.setEncoding('utf8');
|
|
@@ -44,7 +138,7 @@ process.stdin.on('data', chunk => input += chunk);
|
|
|
44
138
|
process.stdin.on('end', () => {
|
|
45
139
|
clearTimeout(stdinTimeout);
|
|
46
140
|
try {
|
|
47
|
-
const data = JSON.parse(input);
|
|
141
|
+
const data = normalizeKimiPayload(JSON.parse(input));
|
|
48
142
|
const toolName = data.tool_name;
|
|
49
143
|
|
|
50
144
|
// Only guard Edit, Write, and MultiEdit tool calls
|
|
@@ -72,13 +166,14 @@ process.stdin.on('end', () => {
|
|
|
72
166
|
}
|
|
73
167
|
|
|
74
168
|
// #1342: Only enforce inside a GSD-managed isolated executor worktree. Those
|
|
75
|
-
// are always on
|
|
76
|
-
// by worktree-branch-check.md, #2924). A manually-
|
|
77
|
-
// non-GSD work, e.g. Claude Code plan-mode) is
|
|
78
|
-
// guard must be a no-op there. Detached HEAD
|
|
169
|
+
// are always on an `agent-*` or legacy `worktree-agent-*` branch (the positive
|
|
170
|
+
// allow-list enforced by worktree-branch-check.md, #2924, #1995). A manually-
|
|
171
|
+
// created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
|
|
172
|
+
// on the user's own branch, so the guard must be a no-op there. Detached HEAD
|
|
173
|
+
// / error → not GSD-managed → no-op.
|
|
79
174
|
const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
|
|
80
175
|
const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
|
|
81
|
-
if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch)) {
|
|
176
|
+
if (!/^(worktree-)?agent-[A-Za-z0-9._/-]+$/.test(branch)) {
|
|
82
177
|
process.exit(0); // not a GSD-managed executor worktree — no-op
|
|
83
178
|
}
|
|
84
179
|
|
|
@@ -91,12 +186,37 @@ process.stdin.on('end', () => {
|
|
|
91
186
|
}
|
|
92
187
|
const wtTopRaw = wtTopResult.stdout.trim();
|
|
93
188
|
|
|
94
|
-
|
|
189
|
+
// #2595 (review Major 3): read the field TYPED. `?.file_path || ''` let a
|
|
190
|
+
// non-string through — `[]` and `{}` are truthy, so they survived the
|
|
191
|
+
// `!rawFilePath` check and threw inside path.isAbsolute() below, landing in
|
|
192
|
+
// this script's outer `catch { process.exit(0) }`. That is the same
|
|
193
|
+
// crash-to-allow #2547 closes elsewhere, reached through the guard's own
|
|
194
|
+
// read rather than through normalization, and it is NOT closed by making
|
|
195
|
+
// `path` authoritative: normalization returns early for native Claude Code
|
|
196
|
+
// payloads (KIMI_TOOL_NAMES has no 'Edit' entry), so `{"tool_name":"Edit",
|
|
197
|
+
// "tool_input":{"file_path":[]}}` reached it untouched — this guard's
|
|
198
|
+
// original #260 surface. Same shape as hooks/gsd-windsurf-pre-write.js:75.
|
|
199
|
+
const rawFilePath = typeof data.tool_input?.file_path === 'string'
|
|
200
|
+
? data.tool_input.file_path
|
|
201
|
+
: '';
|
|
95
202
|
if (!rawFilePath) {
|
|
96
203
|
process.exit(0);
|
|
97
204
|
}
|
|
98
205
|
|
|
99
|
-
// Relative paths
|
|
206
|
+
// Relative paths resolve against the tool's CWD, which is inside the worktree
|
|
207
|
+
// — so under the runtime this guard was written for they cannot leave it.
|
|
208
|
+
//
|
|
209
|
+
// #2595 (review Minor 5) — state the premise rather than leave it implicit,
|
|
210
|
+
// because THIS PR is what widened the guard's reach to Kimi. "Always safe"
|
|
211
|
+
// holds only while every runtime reaching here either rejects relative paths
|
|
212
|
+
// or resolves them against the worktree CWD. Claude Code's Edit/Write require
|
|
213
|
+
// an absolute file_path, so the original #260 surface satisfies it by
|
|
214
|
+
// construction. kimi-cli's StrReplaceFile takes `path` with no documented
|
|
215
|
+
// absoluteness guarantee, and its resolution behaviour is NOT verified here
|
|
216
|
+
// (no source available to this repo at 4a550ef beyond the schema). If it
|
|
217
|
+
// resolves relative paths against anything other than the tool CWD, a
|
|
218
|
+
// `../`-laden path exits 0 at this line and escapes the worktree. Stating a
|
|
219
|
+
// mechanism and an unverified premise — not asserting a live bypass.
|
|
100
220
|
if (!path.isAbsolute(rawFilePath)) {
|
|
101
221
|
process.exit(0);
|
|
102
222
|
}
|
|
@@ -151,6 +271,8 @@ process.stdin.on('end', () => {
|
|
|
151
271
|
`absolute path is not permitted from an isolated executor worktree. Use a relative path.`,
|
|
152
272
|
};
|
|
153
273
|
process.stdout.write(JSON.stringify(output));
|
|
274
|
+
// Kimi feeds stderr (not stdout) back to the model on exit 2.
|
|
275
|
+
process.stderr.write(output.reason);
|
|
154
276
|
process.exit(2);
|
|
155
277
|
}
|
|
156
278
|
// Outside all git repositories — fail open (#1342).
|
|
@@ -177,6 +299,8 @@ process.stdin.on('end', () => {
|
|
|
177
299
|
};
|
|
178
300
|
|
|
179
301
|
process.stdout.write(JSON.stringify(output));
|
|
302
|
+
// Kimi feeds stderr (not stdout) back to the model on exit 2.
|
|
303
|
+
process.stderr.write(output.reason);
|
|
180
304
|
process.exit(2);
|
|
181
305
|
} catch {
|
|
182
306
|
// Silent fail — never block valid tool calls due to hook errors
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// cursor-workspace.js — shared workspace resolution for Cursor lifecycle hooks (#2587).
|
|
2
|
+
//
|
|
3
|
+
// No `gsd-hook-version:` marker, deliberately — matching hooks/lib/git-cmd.js.
|
|
4
|
+
// Neither copy path that stages hooks/lib/*.js substitutes the version
|
|
5
|
+
// placeholder (copyLibDir stamps .sh files only), and the managed-hooks
|
|
6
|
+
// staleness scan covers top-level hooks/ names, not hooks/lib/. A marker here
|
|
7
|
+
// would ship to users as an unsubstituted literal.
|
|
8
|
+
//
|
|
9
|
+
// Cursor invokes hooks with cwd set to the Cursor config dir (~/.cursor) under
|
|
10
|
+
// the cursor-agent CLI, NOT the workspace. Every hook that resolves a project
|
|
11
|
+
// path from process.cwd() therefore missed: sessionStart could only emit its
|
|
12
|
+
// "no .planning/ workflow found" nudge, stop's verify-work reminder never fired,
|
|
13
|
+
// and subagentStart left every subagent without phase context.
|
|
14
|
+
//
|
|
15
|
+
// The hook payload carries the real path in `workspace_roots`. This module is
|
|
16
|
+
// the single place that turns that payload into a project root, so the three
|
|
17
|
+
// hooks cannot drift apart (they previously carried three copies of it).
|
|
18
|
+
//
|
|
19
|
+
// Consumers: gsd-cursor-session-start.js, gsd-cursor-stop.js,
|
|
20
|
+
// gsd-cursor-subagent-start.js. Staged into <config>/hooks/lib/ by
|
|
21
|
+
// writeCursorHooksJson (src/runtime-hooks-surface.cts) alongside the scripts
|
|
22
|
+
// that require it, and registered in the installer's GSD_HOOK_LIB_FILES so
|
|
23
|
+
// uninstall and the manifest manage it.
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the workspace root a Cursor hook should report on.
|
|
32
|
+
*
|
|
33
|
+
* Search order:
|
|
34
|
+
* 1. Each entry of `workspace_roots` that actually carries .planning/STATE.md
|
|
35
|
+
* — so a multi-root workspace whose GSD project is not the first root
|
|
36
|
+
* still resolves.
|
|
37
|
+
* 2. process.cwd(), if IT carries .planning/STATE.md. cwd is a CANDIDATE, not
|
|
38
|
+
* merely the empty-roots fallback: an IDE invocation can supply
|
|
39
|
+
* workspace_roots AND run from the project, and searching roots alone
|
|
40
|
+
* would report "absent" for a project sitting right at cwd — narrower than
|
|
41
|
+
* the pre-fix always-cwd behavior this replaces.
|
|
42
|
+
* 3. The first declared root, else cwd — so the "absent" message still names
|
|
43
|
+
* a sensible directory when there is genuinely no project.
|
|
44
|
+
*
|
|
45
|
+
* Never throws: a malformed payload degrades to cwd rather than wedging the
|
|
46
|
+
* session. (fs.existsSync itself does not throw — it returns false for NUL
|
|
47
|
+
* bytes, over-long components, and non-directory ancestors.)
|
|
48
|
+
*
|
|
49
|
+
* @param {string} rawInput Raw stdin payload as received by the hook.
|
|
50
|
+
* @returns {string} Absolute-or-relative directory to resolve .planning/ against.
|
|
51
|
+
*/
|
|
52
|
+
function resolveWorkspaceRoot(rawInput) {
|
|
53
|
+
let input = {};
|
|
54
|
+
try { input = JSON.parse(rawInput || '{}'); } catch { /* fall back to cwd */ }
|
|
55
|
+
const roots = Array.isArray(input.workspace_roots)
|
|
56
|
+
? input.workspace_roots.filter((r) => typeof r === 'string' && r.length > 0)
|
|
57
|
+
: [];
|
|
58
|
+
for (const root of [...roots, process.cwd()]) {
|
|
59
|
+
if (fs.existsSync(path.join(root, '.planning', 'STATE.md'))) return root;
|
|
60
|
+
}
|
|
61
|
+
return roots[0] || process.cwd();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convenience: the .planning/STATE.md path for the resolved workspace.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} rawInput Raw stdin payload as received by the hook.
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function resolveStatePath(rawInput) {
|
|
71
|
+
return path.join(resolveWorkspaceRoot(rawInput), '.planning', 'STATE.md');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { resolveWorkspaceRoot, resolveStatePath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"main": ".opencode/plugins/gsd-core.js",
|
|
6
6
|
"bin": {
|
|
@@ -65,12 +65,14 @@
|
|
|
65
65
|
"eslint-plugin-no-only-tests": "^3.4.0",
|
|
66
66
|
"fast-check": "^4.8.0",
|
|
67
67
|
"globals": "^16.5.0",
|
|
68
|
-
"js-yaml": "^4.2.
|
|
68
|
+
"js-yaml": "^4.2.1",
|
|
69
69
|
"typescript": "^6.0.3",
|
|
70
70
|
"typescript-eslint": "^8.60.0"
|
|
71
71
|
},
|
|
72
72
|
"overrides": {
|
|
73
|
-
"qs": ">=6.15.2"
|
|
73
|
+
"qs": ">=6.15.2",
|
|
74
|
+
"body-parser": ">=2.3.0",
|
|
75
|
+
"@hono/node-server": ">=2.0.5"
|
|
74
76
|
},
|
|
75
77
|
"optionalDependencies": {
|
|
76
78
|
"fallow": "^2.70.0"
|
|
@@ -90,7 +92,8 @@
|
|
|
90
92
|
"gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
|
|
91
93
|
"gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
|
|
92
94
|
"gen:registry": "node scripts/gen-registry.cjs --write",
|
|
93
|
-
"gen:
|
|
95
|
+
"gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs",
|
|
96
|
+
"regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree",
|
|
94
97
|
"validate:registry": "node scripts/validate-registry.cjs",
|
|
95
98
|
"prepack": "npm run build:lib",
|
|
96
99
|
"prepare": "npm run build:lib",
|
|
@@ -101,7 +104,7 @@
|
|
|
101
104
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
102
105
|
"lint:fix": "eslint . --fix",
|
|
103
106
|
"lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
|
|
104
|
-
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs",
|
|
107
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-emitted-drift-ack.cjs && node scripts/lint-portable-timeout.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-test.cjs",
|
|
105
108
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
106
109
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
107
110
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
@@ -109,11 +112,10 @@
|
|
|
109
112
|
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
|
110
113
|
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
|
111
114
|
"lint:changeset": "node scripts/changeset/lint.cjs",
|
|
112
|
-
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
|
|
115
|
+
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check",
|
|
113
116
|
"lint:docs": "node scripts/lint-docs-required.cjs",
|
|
114
117
|
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
|
|
115
118
|
"ci:test-scope": "node scripts/ci-test-scope.cjs",
|
|
116
|
-
"size:baseline": "node scripts/update-size-baseline.cjs",
|
|
117
119
|
"changeset": "node scripts/changeset/new.cjs",
|
|
118
120
|
"changelog:render": "node scripts/changeset/cli.cjs render",
|
|
119
121
|
"test": "node scripts/run-tests.cjs",
|
|
@@ -125,7 +127,7 @@
|
|
|
125
127
|
"test:affected": "node scripts/run-affected-tests.cjs",
|
|
126
128
|
"test:coverage": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
|
|
127
129
|
"test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
|
|
128
|
-
"test:coverage:unit": "c8 --
|
|
130
|
+
"test:coverage:unit": "c8 --reporter text --reporter json-summary --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && node scripts/check-coverage-gate.cjs",
|
|
129
131
|
"test:coverage:all": "npm run test:coverage",
|
|
130
132
|
"test:mutation": "stryker run",
|
|
131
133
|
"test:mutation:since": "stryker run --incremental --since origin/next"
|
package/pi/gsd.cjs
CHANGED
|
@@ -9,8 +9,14 @@
|
|
|
9
9
|
* This extension binds GSD's command surface to pi via the imperative adapter
|
|
10
10
|
* path — the programmatic-CLI peer of the OpenCode worked binding.
|
|
11
11
|
*
|
|
12
|
-
* Installation: copy this file to ~/.pi/agent/extensions/gsd.
|
|
13
|
-
*
|
|
12
|
+
* Installation: copy this file to ~/.pi/agent/extensions/gsd.js — note the
|
|
13
|
+
* `.js` DEST suffix, not `.cjs`. pi auto-discovers extensions/ entries through
|
|
14
|
+
* `isExtensionFile()`, which accepts only `.ts`/`.js` and skips anything else
|
|
15
|
+
* SILENTLY (no error, no log line), so a `.cjs` dest installs fine and is then
|
|
16
|
+
* never loaded (#2470). This source file keeps its `.cjs` suffix on purpose —
|
|
17
|
+
* tests `require()` it directly and `.cjs` is unambiguous CommonJS — and pi
|
|
18
|
+
* loads the copied file via jiti, which handles CommonJS and ESM alike, so the
|
|
19
|
+
* suffix gates discovery, not parsing. The engine is resolved from the installed
|
|
14
20
|
* GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
|
|
15
21
|
* (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
|
|
16
22
|
* capabilities/pi/capability.json does NOT set
|
|
@@ -191,10 +197,35 @@ function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) {
|
|
|
191
197
|
return async function onBeforeProviderRequest(event, ctx) {
|
|
192
198
|
try {
|
|
193
199
|
const effectiveCwd = (ctx && ctx.cwd) || process.cwd();
|
|
194
|
-
const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
|
|
195
200
|
const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs'));
|
|
196
201
|
const config = loadConfig(effectiveCwd);
|
|
197
202
|
const overrides = (config && config.model_profile_overrides) || undefined;
|
|
203
|
+
|
|
204
|
+
// #2460: FAIL-OPEN when the user has not explicitly configured
|
|
205
|
+
// model_profile_overrides[runtime][tier]. pi is provider-agnostic
|
|
206
|
+
// (kimi-coding, zai, openrouter, openai-codex, minimax, anthropic, …);
|
|
207
|
+
// the built-in RUNTIME_PROFILE_MAP.pi.sonnet default is `claude-sonnet-5`,
|
|
208
|
+
// an Anthropic-ecosystem assumption that is wrong for every non-Anthropic
|
|
209
|
+
// provider. Returning the built-in default unconditionally rewrote every
|
|
210
|
+
// outgoing request to a model the active provider did not know.
|
|
211
|
+
//
|
|
212
|
+
// resolveTierEntry falls back to the built-in catalog when no override is
|
|
213
|
+
// present, so we cannot call it to detect "did the user opt in?" — we
|
|
214
|
+
// inspect the override map directly. Only when the user has explicitly
|
|
215
|
+
// set model_profile_overrides[runtime][tier] do we steer; otherwise we
|
|
216
|
+
// return undefined and pi's chosen model flows through untouched.
|
|
217
|
+
const userRaw = overrides && overrides.pi ? overrides.pi[tier] : undefined;
|
|
218
|
+
// `''` is treated the same as null/undefined: an explicit empty-string
|
|
219
|
+
// override is the user clearing a previously-set value, NOT opting in to
|
|
220
|
+
// steering. Without this guard, `resolveTierEntry`'s falsy `if (userRaw)`
|
|
221
|
+
// check would silently fall back to the built-in catalog and rewrite
|
|
222
|
+
// payload.model to claude-sonnet-5 — re-introducing the exact bug this
|
|
223
|
+
// handler exists to prevent.
|
|
224
|
+
if (userRaw === undefined || userRaw === null || userRaw === '') {
|
|
225
|
+
return undefined; // fail-open — leave pi's model untouched
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
|
|
198
229
|
const entry = resolveTierEntry({ runtime: 'pi', tier, overrides });
|
|
199
230
|
const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null;
|
|
200
231
|
if (!modelId) return undefined; // fail-open — leave pi's model untouched
|
|
@@ -65,10 +65,23 @@ function extractDocsExempt(body) {
|
|
|
65
65
|
// is CRLF-aware so Windows-authored fragments don't leave residual `\r`
|
|
66
66
|
// characters that would shift the `(#NNNN)` PR suffix to a blank line in
|
|
67
67
|
// the rendered CHANGELOG.md / GitHub release-notes bullet.
|
|
68
|
+
//
|
|
69
|
+
// Both leading AND trailing line terminators are stripped. `DOCS_EXEMPT_RE`
|
|
70
|
+
// removes the marker's own text but its `$` anchor (multiline mode) does
|
|
71
|
+
// not consume the `\n` that terminates the marker's line. When the marker
|
|
72
|
+
// is the FIRST line of the body, that leftover `\n` becomes the new first
|
|
73
|
+
// character of `body` — serializeChangelog then emits an empty `- ` bullet
|
|
74
|
+
// followed by an orphaned continuation paragraph, and parseChangelog's
|
|
75
|
+
// bullet-continuation check (which requires a leading `\s`) treats that
|
|
76
|
+
// non-indented paragraph as terminating the bullet, silently dropping the
|
|
77
|
+
// entry's content on re-parse. Stripping leading terminators here closes
|
|
78
|
+
// that gap the same way the trailing strip already does for the opposite
|
|
79
|
+
// (marker-last) position.
|
|
68
80
|
const cleaned = body
|
|
69
81
|
.replace(DOCS_EXEMPT_RE, '')
|
|
70
82
|
.replace(/[ \t\r]+$/gm, '') // strip trailing \r/spaces on each line
|
|
71
83
|
.replace(/(?:\r?\n){3,}/g, '\n\n') // collapse 3+ blank lines (CRLF-aware)
|
|
84
|
+
.replace(/^[\r\n]+/, '') // strip terminators left by a first-line marker
|
|
72
85
|
.replace(/[\r\n]+$/, ''); // strip every trailing line terminator
|
|
73
86
|
return { docsExempt: reason, body: cleaned };
|
|
74
87
|
}
|
|
@@ -105,6 +118,19 @@ function parseFragment(src) {
|
|
|
105
118
|
if (body.endsWith('\r\n')) verbatimBody = body.slice(0, -2);
|
|
106
119
|
else if (body.endsWith('\n')) verbatimBody = body.slice(0, -1);
|
|
107
120
|
else verbatimBody = body;
|
|
121
|
+
// Some fragments have a blank line between the closing frontmatter `---`
|
|
122
|
+
// and the first line of actual content (purely a stylistic authoring
|
|
123
|
+
// choice — the blank line carries no significant content, unlike
|
|
124
|
+
// indentation inside a code block). Strip any such leading blank line(s)
|
|
125
|
+
// here, mirroring the trailing-terminator strip above. Without this,
|
|
126
|
+
// `body` starts with `\n`/`\r\n`, serializeChangelog emits an empty `- `
|
|
127
|
+
// bullet followed by an orphaned paragraph, and parseChangelog's
|
|
128
|
+
// continuation check (requires a leading `\s` on the line) treats that
|
|
129
|
+
// non-indented paragraph as terminating the bullet — silently dropping
|
|
130
|
+
// the fragment's content on re-parse. This is the same downstream failure
|
|
131
|
+
// mode as a first-line docs-exempt marker (see extractDocsExempt below);
|
|
132
|
+
// it just arises from plain authoring whitespace instead of a marker.
|
|
133
|
+
verbatimBody = verbatimBody.replace(/^(?:[ \t]*\r?\n)+/, '');
|
|
108
134
|
const { docsExempt, body: visibleBody } = extractDocsExempt(verbatimBody);
|
|
109
135
|
if (!visibleBody.trim()) return { ok: false, reason: FRAGMENT_ERROR.EMPTY_BODY };
|
|
110
136
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const summaryPath = path.join(process.cwd(), 'coverage', 'coverage-summary.json');
|
|
8
|
+
if (!fs.existsSync(summaryPath)) {
|
|
9
|
+
console.error('check-coverage-gate: coverage/coverage-summary.json not found.');
|
|
10
|
+
console.error('Ensure c8 runs with --reporter json-summary before this script.');
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
|
16
|
+
const errors = [];
|
|
17
|
+
|
|
18
|
+
const OVERALL_LINES = 70;
|
|
19
|
+
const OVERALL_BRANCHES = 60;
|
|
20
|
+
const PER_FILE_BRANCHES = 70;
|
|
21
|
+
const PER_FILE_FILES = [
|
|
22
|
+
'gsd-core/bin/lib/state.cjs',
|
|
23
|
+
'gsd-core/bin/lib/phase.cjs',
|
|
24
|
+
'gsd-core/bin/lib/verify.cjs',
|
|
25
|
+
'gsd-core/bin/lib/init.cjs',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const overall = summary.total;
|
|
29
|
+
if (overall.lines.pct < OVERALL_LINES) {
|
|
30
|
+
errors.push(`Overall lines ${overall.lines.pct}% < ${OVERALL_LINES}%`);
|
|
31
|
+
}
|
|
32
|
+
if (overall.branches.pct < OVERALL_BRANCHES) {
|
|
33
|
+
errors.push(`Overall branches ${overall.branches.pct}% < ${OVERALL_BRANCHES}%`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const file of PER_FILE_FILES) {
|
|
37
|
+
const key = Object.keys(summary).find((k) => k.endsWith(file));
|
|
38
|
+
if (!key) {
|
|
39
|
+
errors.push(`${file}: not found in coverage summary`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const pct = summary[key].branches.pct;
|
|
43
|
+
if (pct < PER_FILE_BRANCHES) {
|
|
44
|
+
errors.push(`${file}: branches ${pct}% < ${PER_FILE_BRANCHES}%`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (errors.length > 0) {
|
|
49
|
+
for (const e of errors) console.error(`ERROR: ${e}`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|