@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-compiled-artifact-sync — fail when a *tracked* compiled artifact under
|
|
4
|
+
* gsd-core/bin/lib/ has drifted from its src/*.cts source.
|
|
5
|
+
*
|
|
6
|
+
* ADR-457 compiles src/*.cts to gsd-core/bin/lib/*.cjs at build time and expects
|
|
7
|
+
* those artifacts to be gitignored. Most are (see .gitignore). A handful are
|
|
8
|
+
* still tracked because the migration that moved the module into src/ did not
|
|
9
|
+
* also add the emitted .cjs to .gitignore. While a compiled artifact remains
|
|
10
|
+
* tracked, the committed bytes are what ships to anyone who reads the repo
|
|
11
|
+
* without building — so they must match the source.
|
|
12
|
+
*
|
|
13
|
+
* #2653: gsd-core/bin/lib/api-coverage.cjs sat four days behind its .cts after
|
|
14
|
+
* PR #2551 changed the source without regenerating the artifact, shipping a
|
|
15
|
+
* module that silently lacked the entire #2366 fix while CI stayed green.
|
|
16
|
+
*
|
|
17
|
+
* This check is deliberately regime-agnostic: it asserts a property of whatever
|
|
18
|
+
* is tracked right now. If the remaining artifacts are later untracked and
|
|
19
|
+
* gitignored (the ADR-457 end state), the tracked set becomes empty and this
|
|
20
|
+
* script passes trivially — no edit required.
|
|
21
|
+
*
|
|
22
|
+
* Usage: node scripts/lint-compiled-artifact-sync.cjs [--check]
|
|
23
|
+
* Exit 0 when every tracked artifact matches a fresh compile; 1 otherwise.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const { execFileSync } = require('node:child_process');
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
34
|
+
const LIB_DIR = path.join('gsd-core', 'bin', 'lib');
|
|
35
|
+
const SRC_DIR = 'src';
|
|
36
|
+
|
|
37
|
+
/** Frozen reason codes so tests assert on structure, not prose. */
|
|
38
|
+
const REASON = Object.freeze({
|
|
39
|
+
OK: 'ok_artifacts_in_sync',
|
|
40
|
+
DRIFTED: 'fail_artifact_drifted',
|
|
41
|
+
BUILD_FAILED: 'fail_build_failed',
|
|
42
|
+
MISSING_EMIT: 'fail_missing_emit',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function git(args) {
|
|
46
|
+
return execFileSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tracked .cjs files under gsd-core/bin/lib/ that have a matching src/*.cts.
|
|
51
|
+
* Uses `git ls-files` so the set is derived from what git actually tracks
|
|
52
|
+
* rather than a hand-maintained list that could itself drift.
|
|
53
|
+
*/
|
|
54
|
+
function trackedCompiledArtifacts() {
|
|
55
|
+
const tracked = git(['ls-files', LIB_DIR]).split('\n').filter((l) => l.endsWith('.cjs'));
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const rel of tracked) {
|
|
58
|
+
const stem = path.basename(rel, '.cjs');
|
|
59
|
+
const subdir = path.dirname(path.relative(LIB_DIR, rel));
|
|
60
|
+
const srcRel = path.join(SRC_DIR, subdir === '.' ? '' : subdir, `${stem}.cts`);
|
|
61
|
+
if (fs.existsSync(path.join(REPO_ROOT, srcRel))) out.push({ artifact: rel, source: srcRel });
|
|
62
|
+
}
|
|
63
|
+
return out.sort((a, b) => (a.artifact < b.artifact ? -1 : a.artifact > b.artifact ? 1 : 0));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Compile the whole project to a throwaway outDir so the work tree is untouched. */
|
|
67
|
+
function compileToTemp() {
|
|
68
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-artifact-sync-'));
|
|
69
|
+
try {
|
|
70
|
+
execFileSync(
|
|
71
|
+
process.execPath,
|
|
72
|
+
[
|
|
73
|
+
path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
74
|
+
'-p', path.join(REPO_ROOT, 'tsconfig.build.json'),
|
|
75
|
+
'--outDir', tmp,
|
|
76
|
+
// A throwaway outDir must not reuse the in-tree incremental state, or
|
|
77
|
+
// tsc skips emit for files it believes are already current.
|
|
78
|
+
'--incremental', 'false',
|
|
79
|
+
'--tsBuildInfoFile', 'null',
|
|
80
|
+
],
|
|
81
|
+
{ cwd: REPO_ROOT, encoding: 'utf8', stdio: 'pipe' },
|
|
82
|
+
);
|
|
83
|
+
return { ok: true, dir: tmp };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
86
|
+
const detail = [err.stdout, err.stderr].filter(Boolean).join('\n').trim();
|
|
87
|
+
return { ok: false, detail };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function main() {
|
|
92
|
+
const pairs = trackedCompiledArtifacts();
|
|
93
|
+
if (pairs.length === 0) {
|
|
94
|
+
console.log('ok compiled-artifact-sync: no tracked compiled artifacts (ADR-457 end state)');
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const build = compileToTemp();
|
|
99
|
+
if (!build.ok) {
|
|
100
|
+
console.error(`FAIL compiled-artifact-sync: ${REASON.BUILD_FAILED}`);
|
|
101
|
+
console.error(build.detail);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const drifted = [];
|
|
106
|
+
const missing = [];
|
|
107
|
+
try {
|
|
108
|
+
for (const { artifact, source } of pairs) {
|
|
109
|
+
const fresh = path.join(build.dir, path.relative(LIB_DIR, artifact));
|
|
110
|
+
if (!fs.existsSync(fresh)) { missing.push({ artifact, source }); continue; }
|
|
111
|
+
const a = fs.readFileSync(path.join(REPO_ROOT, artifact));
|
|
112
|
+
const b = fs.readFileSync(fresh);
|
|
113
|
+
if (!a.equals(b)) drifted.push({ artifact, source, committed: a.length, expected: b.length });
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
fs.rmSync(build.dir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
console.error(`FAIL compiled-artifact-sync: ${REASON.MISSING_EMIT}`);
|
|
121
|
+
for (const m of missing) console.error(` ${m.artifact} — no emit produced from ${m.source}`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (drifted.length > 0) {
|
|
126
|
+
console.error(`FAIL compiled-artifact-sync: ${REASON.DRIFTED}`);
|
|
127
|
+
for (const d of drifted) {
|
|
128
|
+
console.error(` ${d.artifact} (${d.committed} bytes) != compile of ${d.source} (${d.expected} bytes)`);
|
|
129
|
+
}
|
|
130
|
+
console.error('');
|
|
131
|
+
console.error('The committed artifact is what ships to anyone reading the repo without');
|
|
132
|
+
console.error('building, so it must match its source. Fix with:');
|
|
133
|
+
console.error(' npm run build:lib && git add ' + drifted.map((d) => d.artifact).join(' '));
|
|
134
|
+
console.error('');
|
|
135
|
+
console.error('Alternatively, per ADR-457 these artifacts are meant to be gitignored —');
|
|
136
|
+
console.error('untracking them (git rm --cached + .gitignore) also resolves this.');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`ok compiled-artifact-sync: ${pairs.length} tracked artifact(s) match their source`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (require.main === module) process.exitCode = main();
|
|
145
|
+
|
|
146
|
+
module.exports = { REASON, trackedCompiledArtifacts };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-emitted-drift-ack — refuse to merge a broken emitted-drift acknowledgment (#2789).
|
|
6
|
+
*
|
|
7
|
+
* The ack document is read from TWO sides: the working tree (`readAckFile`) and the BASE
|
|
8
|
+
* REF (`readAckFileAtRef`), because an entry already present at the base is spent and may
|
|
9
|
+
* no longer clear a delta. The base-side read fails LOUDLY on a document it cannot parse
|
|
10
|
+
* — it has to, since silently inheriting nothing would leave every entry able to consume
|
|
11
|
+
* a delta, which is the pre-#2789 gate.
|
|
12
|
+
*
|
|
13
|
+
* That makes a corrupt document ON THE BASE BRANCH unusually expensive: it reds every PR
|
|
14
|
+
* that carries an ack until someone repairs it. The real-tree test avoids an outright
|
|
15
|
+
* deadlock (a tree with no ack never consults the base, so the repair PR still lands),
|
|
16
|
+
* but the cheaper answer is to never let a broken document reach the base at all. This
|
|
17
|
+
* runs in `lint:ci`, so a PR carrying one cannot go green and cannot merge.
|
|
18
|
+
*
|
|
19
|
+
* This validator is DELIBERATELY STANDALONE. `scripts/` ships in the npm package and
|
|
20
|
+
* `tests/` does not (package.json `files`), so requiring the gate's own `parseAck` from
|
|
21
|
+
* here would be a MODULE_NOT_FOUND in the published package. The duplication is bounded
|
|
22
|
+
* by a parity test — `tests/emitted-attribution.test.cjs` runs both surfaces over one
|
|
23
|
+
* corpus and fails if they ever disagree about what is schema-valid.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
const ACK_VERSION = 1;
|
|
30
|
+
const ACK_REPO_PATH = 'tests/emitted-drift-ack.json';
|
|
31
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
32
|
+
|
|
33
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate an ack document's raw text.
|
|
37
|
+
*
|
|
38
|
+
* `schemaErrors` are the ones that must agree with the gate's `parseAck` — the shape
|
|
39
|
+
* contract. `policyErrors` are lint-only rules that `parseAck` deliberately does NOT
|
|
40
|
+
* enforce, because they are about what may be COMMITTED rather than what may be parsed:
|
|
41
|
+
* a present-but-entryless document parses fine and signals nothing, so it must be deleted
|
|
42
|
+
* rather than left behind.
|
|
43
|
+
*
|
|
44
|
+
* @param {string|null} raw file contents, or null when the file is absent
|
|
45
|
+
* @returns {{ schemaErrors: string[], policyErrors: string[], ok: boolean }}
|
|
46
|
+
*/
|
|
47
|
+
function validateAckText(raw) {
|
|
48
|
+
const schemaErrors = [];
|
|
49
|
+
const policyErrors = [];
|
|
50
|
+
const done = () => ({ schemaErrors, policyErrors, ok: schemaErrors.length === 0 && policyErrors.length === 0 });
|
|
51
|
+
|
|
52
|
+
if (raw === null) return done(); // absent is the healthy steady state
|
|
53
|
+
|
|
54
|
+
if (raw.trim() === '') {
|
|
55
|
+
schemaErrors.push(`${ACK_REPO_PATH} is present but empty`);
|
|
56
|
+
return done();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let doc;
|
|
60
|
+
try {
|
|
61
|
+
doc = JSON.parse(raw);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
schemaErrors.push(`${ACK_REPO_PATH} is not valid JSON: ${err.message}`);
|
|
64
|
+
return done();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A document that is literally `null` is POLICY, not schema. The gate's `parseAck` uses
|
|
68
|
+
// `null` as its "absent == no acks" sentinel, so it reads such a file as legal and
|
|
69
|
+
// harmless — and the parity test holds us to that. It is still not something to commit:
|
|
70
|
+
// it declares nothing, so the remedy is the same as an entryless document.
|
|
71
|
+
if (doc === null) {
|
|
72
|
+
policyErrors.push(
|
|
73
|
+
`${ACK_REPO_PATH} contains "null" and declares no acknowledgments. Delete the file — `
|
|
74
|
+
+ 'the healthy steady state is no file at all.',
|
|
75
|
+
);
|
|
76
|
+
return done();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isPlainObject(doc)) {
|
|
80
|
+
schemaErrors.push(
|
|
81
|
+
`${ACK_REPO_PATH}: must be a JSON object, got ${Array.isArray(doc) ? 'array' : typeof doc}`,
|
|
82
|
+
);
|
|
83
|
+
return done();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (doc.version !== undefined && doc.version !== ACK_VERSION) {
|
|
87
|
+
schemaErrors.push(
|
|
88
|
+
`${ACK_REPO_PATH}: unsupported version ${JSON.stringify(doc.version)} (expected ${ACK_VERSION})`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const paths = doc.paths;
|
|
93
|
+
if (paths !== undefined && !isPlainObject(paths)) {
|
|
94
|
+
schemaErrors.push(`${ACK_REPO_PATH}: "paths" must be an object of <emitted path> -> { reason }`);
|
|
95
|
+
return done();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entries = paths === undefined ? [] : Object.entries(paths);
|
|
99
|
+
for (const [rel, value] of entries) {
|
|
100
|
+
const reason = isPlainObject(value) ? value.reason : value;
|
|
101
|
+
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
102
|
+
schemaErrors.push(`${ACK_REPO_PATH}: ack for "${rel}" has no non-empty "reason"`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Lint-only. An entryless document parses cleanly and acknowledges nothing, so it is
|
|
107
|
+
// pure confusion on the base branch — and it is exactly what a contributor leaves
|
|
108
|
+
// behind after removing the last entry by hand.
|
|
109
|
+
if (entries.length === 0) {
|
|
110
|
+
policyErrors.push(
|
|
111
|
+
`${ACK_REPO_PATH} is present but declares no acknowledgments. Delete the file — an `
|
|
112
|
+
+ 'empty one signals nothing, and the healthy steady state is no file at all.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return done();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readIfPresent(file) {
|
|
120
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function main() {
|
|
124
|
+
const file = path.join(REPO_ROOT, ...ACK_REPO_PATH.split('/'));
|
|
125
|
+
const result = validateAckText(readIfPresent(file));
|
|
126
|
+
const all = [...result.schemaErrors, ...result.policyErrors];
|
|
127
|
+
|
|
128
|
+
if (all.length) {
|
|
129
|
+
console.error(`lint-emitted-drift-ack: ${all.length} problem(s) in ${ACK_REPO_PATH}\n`);
|
|
130
|
+
for (const e of all) console.error(` - ${e}`);
|
|
131
|
+
console.error(
|
|
132
|
+
'\nThis blocks the merge on purpose. The base-side reader fails loudly on a document '
|
|
133
|
+
+ 'it cannot parse, so a broken one on the base branch reds every PR that carries an '
|
|
134
|
+
+ 'acknowledgment. Fix or delete the file here, where it is cheap.',
|
|
135
|
+
);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(
|
|
141
|
+
fs.existsSync(file)
|
|
142
|
+
? `ok lint-emitted-drift-ack: ${ACK_REPO_PATH} is well-formed`
|
|
143
|
+
: `ok lint-emitted-drift-ack: ${ACK_REPO_PATH} absent (the healthy steady state)`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (require.main === module) main();
|
|
148
|
+
|
|
149
|
+
module.exports = { validateAckText, ACK_VERSION, ACK_REPO_PATH };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-fix-has-regression-test.cjs — gate: every fix(#NNNN) commit must
|
|
6
|
+
* include at least one behavioral test file (tests/*.test.cjs) that is NOT
|
|
7
|
+
* an auto-generated fixture/baseline.
|
|
8
|
+
*
|
|
9
|
+
* ## Why
|
|
10
|
+
*
|
|
11
|
+
* CONTRIBUTING.md:47: "Fix it. Write a test that would have caught the bug."
|
|
12
|
+
* CLAUDE.md: "Regression Protocol: Write the regression test first."
|
|
13
|
+
*
|
|
14
|
+
* Three merged PRs shipped without regression tests because the golden-fixture
|
|
15
|
+
* regeneration (20+ files under tests/) camouflaged the absence of real
|
|
16
|
+
* *.test.cjs changes. This gate makes the rule machine-enforced.
|
|
17
|
+
*
|
|
18
|
+
* ## What this enforces
|
|
19
|
+
*
|
|
20
|
+
* For every commit in `git log origin/next..HEAD` whose subject matches
|
|
21
|
+
* /^fix\(#\d+\)/ or /^feat\(#\d+\)/, the cumulative diff must include at
|
|
22
|
+
* least one file under tests/ matching /\.test\.cjs$/ that is NOT:
|
|
23
|
+
* - under tests/fixtures/
|
|
24
|
+
* - under tests/install-tree/
|
|
25
|
+
* - a *-baseline.json file
|
|
26
|
+
*
|
|
27
|
+
* If the filtered list is empty, the gate fails with a message naming the
|
|
28
|
+
* fix commits and directing the author to add a regression test.
|
|
29
|
+
*
|
|
30
|
+
* ## Overrides
|
|
31
|
+
*
|
|
32
|
+
* Set GSD_SKIP_REGRESSION_TEST_GATE=1 for legitimate exceptions (e.g. a
|
|
33
|
+
* pure-config or pure-refactor PR where no behavioral test is possible).
|
|
34
|
+
* This env var is auditable in CI logs.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const { execSync } = require('child_process');
|
|
38
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
39
|
+
|
|
40
|
+
const FIX_OR_FEAT_RE = /^(?:fix|feat)\(#(\d+)\)/;
|
|
41
|
+
|
|
42
|
+
const EXCLUDE_PATTERNS = [
|
|
43
|
+
/tests[\\/]fixtures[\\/]/,
|
|
44
|
+
/tests[\\/]install-tree[\\/]/,
|
|
45
|
+
/-baseline\.json$/,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function isRealTestFile(filePath) {
|
|
49
|
+
if (!filePath.includes('tests/')) return false;
|
|
50
|
+
if (!/\.test\.cjs$/.test(filePath)) return false;
|
|
51
|
+
return !EXCLUDE_PATTERNS.some((re) => re.test(filePath));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getFixCommits(baseRef) {
|
|
55
|
+
const log = execSync(
|
|
56
|
+
`git log ${baseRef}..HEAD --format='%H%x09%s' --no-merges`,
|
|
57
|
+
{ encoding: 'utf8', timeout: 10000 },
|
|
58
|
+
);
|
|
59
|
+
return log
|
|
60
|
+
.trim()
|
|
61
|
+
.split('\n')
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((line) => {
|
|
64
|
+
const [sha, subject] = line.split('\t');
|
|
65
|
+
const match = subject.match(FIX_OR_FEAT_RE);
|
|
66
|
+
return match ? { sha: sha.slice(0, 12), subject, issue: match[1] } : null;
|
|
67
|
+
})
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getChangedTestFiles(baseRef) {
|
|
72
|
+
const diff = execSync(
|
|
73
|
+
`git diff ${baseRef}...HEAD --name-only --no-merges`,
|
|
74
|
+
{ encoding: 'utf8', timeout: 10000 },
|
|
75
|
+
);
|
|
76
|
+
return diff
|
|
77
|
+
.trim()
|
|
78
|
+
.split('\n')
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.filter(isRealTestFile);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
if (process.env.GSD_SKIP_REGRESSION_TEST_GATE === '1') {
|
|
85
|
+
console.log('lint-fix-has-regression-test: SKIPPED (GSD_SKIP_REGRESSION_TEST_GATE=1)');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const baseRef = process.env.GSD_REGRESSION_GATE_BASE || 'origin/next';
|
|
90
|
+
|
|
91
|
+
let fixCommits;
|
|
92
|
+
try {
|
|
93
|
+
fixCommits = getFixCommits(baseRef);
|
|
94
|
+
} catch {
|
|
95
|
+
console.log(`lint-fix-has-regression-test: no fix/feat commits found vs ${baseRef}, skipping`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (fixCommits.length === 0) {
|
|
100
|
+
console.log('lint-fix-has-regression-test: no fix/feat commits, passing');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let testFiles;
|
|
105
|
+
try {
|
|
106
|
+
testFiles = getChangedTestFiles(baseRef);
|
|
107
|
+
} catch {
|
|
108
|
+
testFiles = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (testFiles.length === 0) {
|
|
112
|
+
const commitList = fixCommits
|
|
113
|
+
.map((c) => ` ${c.sha} ${c.subject}`)
|
|
114
|
+
.join('\n');
|
|
115
|
+
throw new ExitError(1,
|
|
116
|
+
`lint-fix-has-regression-test: ${fixCommits.length} fix/feat commit(s) but ZERO behavioral test files (*.test.cjs) in the diff.\n` +
|
|
117
|
+
`Auto-generated fixtures (tests/fixtures/, *-baseline.json) do NOT count.\n\n` +
|
|
118
|
+
`Fix commits:\n${commitList}\n\n` +
|
|
119
|
+
`CONTRIBUTING.md:47: "Write a test that would have caught the bug."\n` +
|
|
120
|
+
`Add a regression test to an existing tests/*.test.cjs file, or set ` +
|
|
121
|
+
`GSD_SKIP_REGRESSION_TEST_GATE=1 if no behavioral test is possible (auditable in CI).`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(
|
|
126
|
+
`lint-fix-has-regression-test: PASS — ${fixCommits.length} fix/feat commit(s), ` +
|
|
127
|
+
`${testFiles.length} behavioral test file(s): ${testFiles.join(', ')}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
runMain(main);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-portable-timeout.cjs — ban hardcoded GNU-`timeout` in gsd
|
|
6
|
+
* workflow / agent / reference / command markdown (#2351).
|
|
7
|
+
*
|
|
8
|
+
* ## Why
|
|
9
|
+
*
|
|
10
|
+
* `timeout` and `gtimeout` are GNU coreutils. Stock macOS ships NEITHER
|
|
11
|
+
* (`brew install coreutils` only provides `gtimeout`, and only if installed).
|
|
12
|
+
* A hardcoded `timeout <n> <cmd>` inside an executable workflow snippet exits
|
|
13
|
+
* 127 ("command not found") on such a host, and the gate that runs it — which
|
|
14
|
+
* only distinguishes 0 (pass) / 124 (timeout) / other (fail) — misreports a
|
|
15
|
+
* perfectly good build or test command as a FAILURE (#2351).
|
|
16
|
+
*
|
|
17
|
+
* The portable, coreutils-independent replacement is the
|
|
18
|
+
* `gsd_run run-with-timeout <secs> [--] <cmd…>` verb (gsd-core/bin/gsd-tools.cjs):
|
|
19
|
+
* a Node-based wall-clock cap that keeps GNU `timeout`'s exit-code contract
|
|
20
|
+
* (124 on timeout) on every platform. The resolution lives there ONCE and is
|
|
21
|
+
* reused by every call site instead of a per-file `command -v timeout` probe.
|
|
22
|
+
*
|
|
23
|
+
* This ratchet fails the build if a NEW bare `timeout`/`gtimeout` execution
|
|
24
|
+
* slips into any of these surfaces.
|
|
25
|
+
*
|
|
26
|
+
* ## What PASSES
|
|
27
|
+
*
|
|
28
|
+
* - `gsd_run run-with-timeout 300 -- bash -c "$CMD"` — the approved verb.
|
|
29
|
+
* - `command -v timeout` / `command -v gtimeout` / `which timeout` capability
|
|
30
|
+
* PROBES — portable: they detect the binary, they do not unconditionally
|
|
31
|
+
* execute it (see gsd-core/workflows/review.md's `_AGY_KILLER` fallback).
|
|
32
|
+
* - Prose ("timed out after 5 minutes"), config keys
|
|
33
|
+
* (`workflow.test_gate_timeout`), CI `timeout-minutes:`, the agy
|
|
34
|
+
* `--print-timeout` flag, `$TIMEOUT`-style variable names — none of which is
|
|
35
|
+
* a bare timeout command invocation.
|
|
36
|
+
*
|
|
37
|
+
* ## What FAILS
|
|
38
|
+
*
|
|
39
|
+
* A bare `timeout <duration> …` / `gtimeout <duration> …` command invocation,
|
|
40
|
+
* where `<duration>` is a number, a `"$VAR"`, or a `${VAR}` (optionally with a
|
|
41
|
+
* leading `-k`/`-s` option).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
47
|
+
|
|
48
|
+
const ROOT = path.join(__dirname, '..');
|
|
49
|
+
|
|
50
|
+
// Surfaces whose markdown carries agent-executed bash. Kept broad so the guard
|
|
51
|
+
// catches a regression anywhere a workflow snippet could bound a command.
|
|
52
|
+
const DEFAULT_ROOTS = ['gsd-core/workflows', 'gsd-core/references', 'agents', 'commands'];
|
|
53
|
+
|
|
54
|
+
// Capability probes to strip BEFORE testing for an invocation, so a portable
|
|
55
|
+
// `command -v timeout` on the same line is never mistaken for a bare execution.
|
|
56
|
+
const PROBE_RE = /command\s+-v\s+g?timeout|which\s+g?timeout/g;
|
|
57
|
+
|
|
58
|
+
// A `timeout`/`gtimeout` token INVOKED AS A COMMAND with a duration argument.
|
|
59
|
+
// Anchored to a command position — line start or right after `| & ; ( ` {` — so
|
|
60
|
+
// prose ("increase the timeout 30 seconds") and the `--print-timeout` flag / the
|
|
61
|
+
// approved `run-with-timeout` verb (no separator before "timeout") never match.
|
|
62
|
+
// Between the token and the duration, allow any number of leading options in
|
|
63
|
+
// short OR long form (`-k5`, `-k 5`, `--kill-after=5`, `--foreground`,
|
|
64
|
+
// `--signal=KILL`). The duration is a digit, `$((arith))`, a `$VAR`, or `${VAR}`.
|
|
65
|
+
const EXEC_RE = /(?:^|[|&;(`{])[ \t]*g?timeout[ \t]+(?:-{1,2}[\w-]+(?:=\S+)?[ \t]+)*["']?(?:\$\{?[A-Za-z_]|\$\(\(|\d)/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Locate bare `timeout`/`gtimeout` invocations in a block of text.
|
|
69
|
+
*
|
|
70
|
+
* Pure (no I/O): callers pass the file contents; the caller reads files. Returns
|
|
71
|
+
* structured findings so tests assert on typed values, never on grepped text.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} text file contents
|
|
74
|
+
* @returns {{ line: number, snippet: string }[]} findings (empty array = clean)
|
|
75
|
+
*/
|
|
76
|
+
function findRawTimeoutInvocations(text) {
|
|
77
|
+
const findings = [];
|
|
78
|
+
const lines = String(text).split(/\r?\n/);
|
|
79
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
80
|
+
const stripped = lines[i].replace(PROBE_RE, '');
|
|
81
|
+
if (EXEC_RE.test(stripped)) findings.push({ line: i + 1, snippet: lines[i].trim() });
|
|
82
|
+
}
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function walkMarkdown(dir) {
|
|
87
|
+
const out = [];
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
91
|
+
} catch {
|
|
92
|
+
return out; // a missing root is not an error — some surfaces are optional
|
|
93
|
+
}
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const full = path.join(dir, entry.name);
|
|
96
|
+
if (entry.isDirectory()) out.push(...walkMarkdown(full));
|
|
97
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Scan the given roots (repo-relative) for bare timeout invocations.
|
|
104
|
+
* @param {string[]} roots
|
|
105
|
+
* @returns {{ file: string, line: number, snippet: string }[]}
|
|
106
|
+
*/
|
|
107
|
+
function scan(roots = DEFAULT_ROOTS) {
|
|
108
|
+
const offenders = [];
|
|
109
|
+
for (const rel of roots) {
|
|
110
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(ROOT, rel);
|
|
111
|
+
for (const file of walkMarkdown(abs)) {
|
|
112
|
+
const findings = findRawTimeoutInvocations(fs.readFileSync(file, 'utf8'));
|
|
113
|
+
for (const f of findings) {
|
|
114
|
+
offenders.push({ file: path.relative(ROOT, file), line: f.line, snippet: f.snippet });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return offenders;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function main() {
|
|
122
|
+
const rootsEnv = process.env.GSD_LINT_PORTABLE_TIMEOUT_ROOTS;
|
|
123
|
+
const roots = rootsEnv ? rootsEnv.split(path.delimiter).filter(Boolean) : DEFAULT_ROOTS;
|
|
124
|
+
const offenders = scan(roots);
|
|
125
|
+
if (offenders.length > 0) {
|
|
126
|
+
const detail = offenders.map((o) => ` ${o.file}:${o.line} ${o.snippet}`).join('\n');
|
|
127
|
+
throw new ExitError(
|
|
128
|
+
1,
|
|
129
|
+
'lint-portable-timeout: hardcoded `timeout`/`gtimeout` is not portable — stock\n' +
|
|
130
|
+
'macOS ships no coreutils, so these exit 127 and misreport a passing command as a\n' +
|
|
131
|
+
'failure. Use `gsd_run run-with-timeout <secs> [--] <cmd…>` instead (#2351):\n' +
|
|
132
|
+
detail,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
console.log(`ok lint-portable-timeout: no hardcoded timeout invocations in ${roots.length} root(s)`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { findRawTimeoutInvocations, scan, DEFAULT_ROOTS };
|
|
139
|
+
|
|
140
|
+
if (require.main === module) runMain(main);
|
|
@@ -56,6 +56,15 @@ const REGISTRY = [
|
|
|
56
56
|
sourceFile: 'src/init.cts',
|
|
57
57
|
testFile: 'tests/agent-skills.test.cjs',
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
// Registered by #1880 (ADR-1411 amendment "corrupt is not absent"). Until
|
|
61
|
+
// this entry existed the config-loader seam carried the provenance contract
|
|
62
|
+
// with nothing guarding it, so a regression that collapsed configured_empty
|
|
63
|
+
// back into not_configured would have shipped silently.
|
|
64
|
+
verb: 'config-loader',
|
|
65
|
+
sourceFile: 'src/config-loader.cts',
|
|
66
|
+
testFile: 'tests/config-loader.test.cjs',
|
|
67
|
+
},
|
|
59
68
|
];
|
|
60
69
|
|
|
61
70
|
// Markers that MUST appear in every registered verb's test file.
|
|
@@ -145,6 +145,10 @@ const COVERED = {
|
|
|
145
145
|
tests: [
|
|
146
146
|
'tests/frontmatter.property.test.cjs',
|
|
147
147
|
'tests/frontmatter.unit.test.cjs',
|
|
148
|
+
// #1882 added the unterminated-fence detection to frontmatter.cjs, and the tests that
|
|
149
|
+
// constrain it live here. Without this entry the mutants in that branch are covered by
|
|
150
|
+
// no test in the shard, so the module's score drops even though the behaviour is tested.
|
|
151
|
+
'tests/unusable-input.test.cjs',
|
|
148
152
|
],
|
|
149
153
|
minScore: 62,
|
|
150
154
|
},
|
|
@@ -102,6 +102,12 @@ ALLOWLIST=(
|
|
|
102
102
|
# exec command strings (execFileSync('npm', ['install'])) as test DATA the rule
|
|
103
103
|
# must lint — not attack vectors. ADR-1703 Phase 4 (#1726).
|
|
104
104
|
'tests/no-bare-npm-exec.rule.test.cjs'
|
|
105
|
+
# #2547 — the Kimi field-shadowing regression proves gsd-prompt-guard still
|
|
106
|
+
# SCANS the reconstructed edit[].new content when a model-supplied new_string
|
|
107
|
+
# tries to shadow it. The fixture must be a real injection phrase or the test
|
|
108
|
+
# asserts nothing: it is the payload the guard is required to catch, carried
|
|
109
|
+
# as test DATA. Same class as the read-injection-scanner suites above.
|
|
110
|
+
'tests/kimi-payload-field-shadowing.security.test.cjs'
|
|
105
111
|
)
|
|
106
112
|
|
|
107
113
|
is_allowlisted() {
|