@opengsd/gsd-core 1.17.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 -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-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/install.js +38 -2
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +18 -11
- 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 +70 -64
- 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 +6 -4
- package/gsd-core/bin/lib/commands.cjs +5 -1
- 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-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/init.cjs +11 -4
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- 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/phase.cjs +120 -11
- 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 +47 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +24 -16
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +31 -10
- package/gsd-core/bin/lib/state.cjs +90 -60
- 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/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 +5 -1
- 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/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 +37 -6
- 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 +5 -1
- 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-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +72 -14
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +72 -14
- 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/lib/macos-conformance-tier.generated.cjs +9 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +16 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/release-tarball-smoke.cjs +1 -0
- 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
|
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ const AUDIT_FINDING = Object.freeze({
|
|
|
24
24
|
// Match `${GSD_HOME}` or `${GSD_HOME:-...}` followed by a /-rooted path
|
|
25
25
|
// ending in .cjs/.js/.sh. The path is captured verbatim (relative to
|
|
26
26
|
// the install root).
|
|
27
|
-
const REF_RE = /\$\{GSD_HOME(?::-[^}]*)?\}\/([A-Za-z0-9_./-]+\.(?:cjs|js|sh))/g;
|
|
27
|
+
const REF_RE = /\$\{GSD_HOME(?::-[^}]*)?\}\/([A-Za-z0-9_./-]+\.(?:cjs|js|sh)(?![A-Za-z0-9_]))/g;
|
|
28
28
|
|
|
29
29
|
function listWorkflowFiles(dir) {
|
|
30
30
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #5473 — measure the 112KB hook rows INSIDE the real conformance tier.
|
|
5
|
+
*
|
|
6
|
+
* The standalone diagnostic (scripts/diagnose-validate-commit-phases.cjs, #5476)
|
|
7
|
+
* found the hook costs ~0.2 s on a padded 112KB command on macos-latest under
|
|
8
|
+
* /bin/bash 3.2, idle or with four node-forking processes beside it, and no
|
|
9
|
+
* slower than an unpadded body. The conformance job's own row
|
|
10
|
+
* (tests/hooks-opt-in.test.cjs) costs 2.5-5 s per invocation. So whatever makes
|
|
11
|
+
* it slow is a property of how the row runs, not of the command. This module is
|
|
12
|
+
* meant to be called from a test that the tier runs
|
|
13
|
+
* (tests/in-tier-commit-hook-timing.test.cjs), so it sees exactly what the real
|
|
14
|
+
* row sees: the node:test child's environment and the tier's contention.
|
|
15
|
+
*
|
|
16
|
+
* It varies one thing at a time on the same command and reports every cell:
|
|
17
|
+
*
|
|
18
|
+
* - WHICH bash: the real row spawns `bash` by name (so whatever the PATH
|
|
19
|
+
* resolves, possibly a Homebrew bash 5), while the standalone diagnostic
|
|
20
|
+
* used /bin/bash 3.2. Both are timed, and both are named in the report;
|
|
21
|
+
* - the environment handed to the hook (inherited / minimal / without NODE_*
|
|
22
|
+
* / without GSD_* and RUN_TESTS_* / LC_ALL=C / LC_ALL=en_US.UTF-8: bash's
|
|
23
|
+
* pattern substitution is known to be slower in a multibyte locale);
|
|
24
|
+
* - how it is spawned (spawnSync, or asynchronous, detached and killed by
|
|
25
|
+
* process group, as the real row does). Every cell reads stdout and stderr
|
|
26
|
+
* through pipes, as the real row does, so spawn mode is the only difference.
|
|
27
|
+
*
|
|
28
|
+
* Runs are interleaved round-robin across the cells, rotating the starting
|
|
29
|
+
* cell each round, so drift in the tier's load shows up as noise and not as a
|
|
30
|
+
* variant effect. Then one traced run under the PATH bash. It asserts nothing
|
|
31
|
+
* about time. The job environment carries tokens and the report goes to a
|
|
32
|
+
* public log, so no variable name or value is read from the environment into
|
|
33
|
+
* the report: it prints counts, which of a fixed list are set, and what
|
|
34
|
+
* `locale` and `command -v bash` themselves say under that environment.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const crypto = require('node:crypto');
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const os = require('node:os');
|
|
40
|
+
const path = require('node:path');
|
|
41
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
42
|
+
const { stampPs4 } = require('./xtrace-phase-timing.cjs');
|
|
43
|
+
const cli = require('./diagnose-validate-commit-phases.cjs');
|
|
44
|
+
|
|
45
|
+
const SYSTEM_BASH = '/bin/bash';
|
|
46
|
+
const DEFAULT_RUNS = 2;
|
|
47
|
+
const VERSION_PROBE_BOUND_MS = 10000;
|
|
48
|
+
|
|
49
|
+
// Variables whose PRESENCE is worth reporting (a constant list: the report names
|
|
50
|
+
// these, never anything read from the environment).
|
|
51
|
+
const PRESENCE_ENV = [
|
|
52
|
+
'CI', 'TEST_CONCURRENCY', 'NODE_OPTIONS', 'NODE_TEST_CONTEXT', 'NODE_V8_COVERAGE',
|
|
53
|
+
'GSD_PLUGIN_ROOT', 'GSD_STRICT_LIVE_CONFIG_GUARD', 'RUN_TESTS_TIMING_EVENTS_FILE',
|
|
54
|
+
];
|
|
55
|
+
const FAMILIES = ['NODE_', 'GSD_', 'RUN_TESTS_', 'LC_'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What the report may say about an environment: how many variables, how many in
|
|
59
|
+
* each family, and which of PRESENCE_ENV are set. No name and no value is read
|
|
60
|
+
* from the environment into the result, because the job environment carries
|
|
61
|
+
* tokens and a report goes to a public log.
|
|
62
|
+
*/
|
|
63
|
+
function describeEnv(env) {
|
|
64
|
+
const keys = Object.keys(env);
|
|
65
|
+
const families = {};
|
|
66
|
+
for (const prefix of FAMILIES) families[prefix] = keys.filter((k) => k.startsWith(prefix)).length;
|
|
67
|
+
const present = PRESENCE_ENV.filter((name) => Object.hasOwn(env, name));
|
|
68
|
+
return { count: keys.length, families, present };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The effective locale the hook would see under `env`, as `locale` itself reports
|
|
73
|
+
* it (LANG and LC_* lines only). Asking the tool, rather than reading the variables,
|
|
74
|
+
* is also what tells a missing locale from a set one.
|
|
75
|
+
*/
|
|
76
|
+
function localeReport(env) {
|
|
77
|
+
const r = spawnSync('locale', [], { env, encoding: 'utf8', timeout: VERSION_PROBE_BOUND_MS });
|
|
78
|
+
if (r.error || r.status !== 0) return 'unavailable';
|
|
79
|
+
return r.stdout.split('\n').filter((line) => /^(LANG|LC_[A-Z]+)=/.test(line)).join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `env` without any variable whose name starts with one of `prefixes`. */
|
|
83
|
+
function without(env, prefixes) {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [key, value] of Object.entries(env)) {
|
|
86
|
+
if (!prefixes.some((p) => key.startsWith(p))) out[key] = value;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The first executable named `name` on `pathVar`, as `spawn(name)` would resolve it, or null. */
|
|
92
|
+
function findOnPath(name, pathVar) {
|
|
93
|
+
for (const dir of String(pathVar || '').split(path.delimiter)) {
|
|
94
|
+
if (!dir) continue;
|
|
95
|
+
const candidate = path.join(dir, name);
|
|
96
|
+
try {
|
|
97
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
98
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
99
|
+
} catch { /* not here */ }
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The cells to time. `base` is the real row's environment; `bash` is what
|
|
106
|
+
* `spawn('bash')` resolves to under it. A cell is one (interpreter, environment,
|
|
107
|
+
* spawn mode); cells that would repeat another are left out.
|
|
108
|
+
*/
|
|
109
|
+
function buildCells(base, bash) {
|
|
110
|
+
const cells = [];
|
|
111
|
+
const add = (label, bin, env, mode) => cells.push({ label, bin, env, mode });
|
|
112
|
+
add('PATH bash, inherited env', bash, base, 'sync');
|
|
113
|
+
add('PATH bash, inherited env', bash, base, 'async');
|
|
114
|
+
if (bash !== SYSTEM_BASH) {
|
|
115
|
+
add('/bin/bash, inherited env', SYSTEM_BASH, base, 'sync');
|
|
116
|
+
add('/bin/bash, inherited env', SYSTEM_BASH, base, 'async');
|
|
117
|
+
}
|
|
118
|
+
add('PATH bash, minimal env (PATH, HOME)', bash, { PATH: base.PATH, HOME: base.HOME || '' }, 'sync');
|
|
119
|
+
add('PATH bash, without NODE_*', bash, without(base, ['NODE_']), 'sync');
|
|
120
|
+
add('PATH bash, without GSD_* / RUN_TESTS_*', bash, without(base, ['GSD_', 'RUN_TESTS_']), 'sync');
|
|
121
|
+
add('PATH bash, LC_ALL=C', bash, { ...base, LC_ALL: 'C' }, 'sync');
|
|
122
|
+
add('PATH bash, LC_ALL=en_US.UTF-8', bash, { ...base, LC_ALL: 'en_US.UTF-8' }, 'sync');
|
|
123
|
+
return cells;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const elapsedMs = (started) => Number((process.hrtime.bigint() - started) / 1000000n);
|
|
127
|
+
|
|
128
|
+
/** One synchronous run; stdout and stderr go through pipes like the real row's. */
|
|
129
|
+
function runSync(project, command, env, bin) {
|
|
130
|
+
const started = process.hrtime.bigint();
|
|
131
|
+
const r = spawnSync(bin, [cli.HOOK], {
|
|
132
|
+
cwd: project,
|
|
133
|
+
env,
|
|
134
|
+
input: JSON.stringify({ tool_input: { command } }),
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
timeout: cli.HOOK_BOUND_MS,
|
|
137
|
+
killSignal: 'SIGKILL',
|
|
138
|
+
});
|
|
139
|
+
return { wallMs: elapsedMs(started), status: r.status, signal: r.signal, killed: r.signal === 'SIGKILL' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* One run the way the real row does it (tests/hooks-opt-in.test.cjs runCmdAsync):
|
|
144
|
+
* asynchronous, detached, stdout and stderr read through pipes, killed by
|
|
145
|
+
* process group at the bound.
|
|
146
|
+
*/
|
|
147
|
+
function runDetached(project, command, env, bin) {
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const started = process.hrtime.bigint();
|
|
150
|
+
const child = spawn(bin, [cli.HOOK], { cwd: project, env, detached: true });
|
|
151
|
+
let settled = false;
|
|
152
|
+
let killed = false;
|
|
153
|
+
const killer = setTimeout(() => {
|
|
154
|
+
killed = true;
|
|
155
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* gone */ } }
|
|
156
|
+
}, cli.HOOK_BOUND_MS);
|
|
157
|
+
const settle = (result) => {
|
|
158
|
+
if (settled) return;
|
|
159
|
+
settled = true;
|
|
160
|
+
clearTimeout(killer);
|
|
161
|
+
resolve({ wallMs: elapsedMs(started), killed, ...result });
|
|
162
|
+
};
|
|
163
|
+
child.on('error', (err) => settle({ status: null, signal: null, error: err.code || err.message }));
|
|
164
|
+
child.stdin.on('error', () => { /* the hook may exit before reading stdin */ });
|
|
165
|
+
child.stdout.on('data', () => {});
|
|
166
|
+
child.stderr.on('data', () => {});
|
|
167
|
+
child.on('close', (status, signal) => settle({ status, signal }));
|
|
168
|
+
child.stdin.end(JSON.stringify({ tool_input: { command } }));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const summarize = (walls) => {
|
|
173
|
+
const sorted = [...walls].sort((a, b) => a - b);
|
|
174
|
+
return `min ${sorted[0]} / median ${cli.median(sorted)} / max ${sorted[sorted.length - 1]}`;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function versionLine(bin) {
|
|
178
|
+
const r = spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: VERSION_PROBE_BOUND_MS });
|
|
179
|
+
return (r.stdout || 'unknown').split('\n')[0];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The order to visit `count` cells in round `round`: every cell once, starting one further along each round. */
|
|
183
|
+
function roundOrder(count, round) {
|
|
184
|
+
return Array.from({ length: count }, (_, k) => (k + round) % count);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Where the shell itself resolves `bash` under `env` (its answer is what the report prints). */
|
|
188
|
+
function resolveBash(env) {
|
|
189
|
+
const r = spawnSync('/bin/sh', ['-c', 'command -v bash'], { env, encoding: 'utf8', timeout: VERSION_PROBE_BOUND_MS });
|
|
190
|
+
return r.status === 0 ? r.stdout.trim() : 'not found';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Run every cell `runs` times, round-robin, rotating the starting cell each round. */
|
|
194
|
+
async function measureCells(project, command, cells, runs) {
|
|
195
|
+
const walls = cells.map(() => []);
|
|
196
|
+
const last = cells.map(() => null);
|
|
197
|
+
for (let round = 0; round < runs; round++) {
|
|
198
|
+
for (const i of roundOrder(cells.length, round)) {
|
|
199
|
+
const cell = cells[i];
|
|
200
|
+
const result = cell.mode === 'async'
|
|
201
|
+
? await runDetached(project, command, cell.env, cell.bin)
|
|
202
|
+
: runSync(project, command, cell.env, cell.bin);
|
|
203
|
+
walls[i].push(result.wallMs);
|
|
204
|
+
last[i] = result;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return cells.map((cell, i) => ({ cell, walls: walls[i], last: last[i] }));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {{ runs?: number, scenarioList?: { name: string, command: string }[] }} [options]
|
|
212
|
+
* scenarioList defaults to the padded + trailing-option row and the unpadded comparison.
|
|
213
|
+
* @returns {Promise<{ text: string, ok: boolean }>} ok is false if the traced run produced no trace.
|
|
214
|
+
*/
|
|
215
|
+
async function inTierReport({ runs = DEFAULT_RUNS, scenarioList } = {}) {
|
|
216
|
+
if (!Number.isInteger(runs) || runs < 1) throw new RangeError('runs must be a positive integer');
|
|
217
|
+
const all = cli.scenarios();
|
|
218
|
+
const picked = scenarioList || [all[1], all[2]];
|
|
219
|
+
const base = cli.hookEnv();
|
|
220
|
+
const bash = findOnPath('bash', base.PATH) || SYSTEM_BASH;
|
|
221
|
+
const cells = buildCells(base, bash);
|
|
222
|
+
const nonce = crypto.randomBytes(6).toString('hex');
|
|
223
|
+
const desc = describeEnv(process.env);
|
|
224
|
+
const out = ['# validate-commit INSIDE the conformance tier (#5473)', ''];
|
|
225
|
+
out.push(`- host: ${os.type()} ${os.release()} ${process.arch}, ${os.cpus().length} cpus, load average ${os.loadavg().map((n) => n.toFixed(2)).join(' ')}, node ${process.version}`);
|
|
226
|
+
out.push(`- \`bash\` on the inherited PATH: ${resolveBash(base)} (${versionLine(bash)})`);
|
|
227
|
+
out.push(`- /bin/bash: ${versionLine(SYSTEM_BASH)}`);
|
|
228
|
+
out.push(`- environment: ${desc.count} variables (${FAMILIES.map((p) => `${p}* ${desc.families[p]}`).join(', ')}); set: ${desc.present.join(' ') || 'none of the watched ones'}`);
|
|
229
|
+
out.push(`- effective locale: ${localeReport(base)}`);
|
|
230
|
+
out.push(`- ${runs} runs per cell, interleaved round-robin across cells with a rotating start`, '');
|
|
231
|
+
let project;
|
|
232
|
+
let traced;
|
|
233
|
+
try {
|
|
234
|
+
project = cli.makeProject();
|
|
235
|
+
const hooks = cli.prepareTracedHooks(project, stampPs4(cli.STAMP_COMMAND, nonce));
|
|
236
|
+
for (const scenario of picked) {
|
|
237
|
+
out.push(`## ${scenario.name} (${scenario.command.length} bytes)`, '');
|
|
238
|
+
out.push(`| interpreter, environment | spawn | wall ms (${runs} runs) | last exit |`, '|---|---|---|---|');
|
|
239
|
+
for (const m of await measureCells(project, scenario.command, cells, runs)) {
|
|
240
|
+
const mode = m.cell.mode === 'async' ? 'async + detached + group kill' : 'spawnSync';
|
|
241
|
+
const exit = `${m.last.status}${m.last.signal ? ` ${m.last.signal}` : ''}${m.last.killed ? ' KILLED' : ''}`;
|
|
242
|
+
out.push(`| ${m.cell.label} | ${mode} | ${summarize(m.walls)} | ${exit} |`);
|
|
243
|
+
}
|
|
244
|
+
out.push('');
|
|
245
|
+
}
|
|
246
|
+
traced = cli.measureScenario(project, picked[0], hooks, nonce, bash);
|
|
247
|
+
} finally {
|
|
248
|
+
if (project) fs.rmSync(project, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
out.push('## traced run under the inherited environment (PATH bash)', '');
|
|
251
|
+
out.push(...cli.renderScenario(traced, fs.readFileSync(cli.HOOK, 'utf8').split('\n'), { top: 12 }));
|
|
252
|
+
out.push(`- load average after: ${os.loadavg().map((n) => n.toFixed(2)).join(' ')}`);
|
|
253
|
+
return { text: out.join('\n'), ok: traced.entries > 0 };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
describeEnv,
|
|
258
|
+
localeReport,
|
|
259
|
+
without,
|
|
260
|
+
findOnPath,
|
|
261
|
+
buildCells,
|
|
262
|
+
roundOrder,
|
|
263
|
+
runSync,
|
|
264
|
+
runDetached,
|
|
265
|
+
measureCells,
|
|
266
|
+
inTierReport,
|
|
267
|
+
PRESENCE_ENV,
|
|
268
|
+
FAMILIES,
|
|
269
|
+
SYSTEM_BASH,
|
|
270
|
+
};
|