godpowers 3.11.0 → 3.13.1
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/CHANGELOG.md +107 -0
- package/README.md +23 -10
- package/RELEASE.md +22 -22
- package/agents/god-debt-assessor.md +179 -99
- package/bin/install.js +1 -16
- package/hooks/pre-tool-use.sh +52 -40
- package/lib/README.md +11 -1
- package/lib/artifact-map.js +6 -0
- package/lib/cli-dispatch.js +29 -20
- package/lib/cli-log.js +24 -0
- package/lib/dashboard.js +1 -10
- package/lib/evidence.js +50 -13
- package/lib/gate.js +2 -2
- package/lib/installer-args.js +140 -290
- package/lib/installer-core.js +1 -12
- package/lib/planning-systems.js +1 -4
- package/lib/recipe-coverage-sync.js +1 -11
- package/lib/release-surface-sync.js +2 -20
- package/lib/repo-doc-sync.js +1 -16
- package/lib/repo-surface-sync.js +1 -24
- package/lib/requirements.js +2 -5
- package/lib/route-quality-sync.js +1 -12
- package/lib/state.js +19 -8
- package/lib/sync-fs.js +37 -0
- package/lib/text-util.js +19 -0
- package/lib/workflow-helper-groups.js +4 -0
- package/package.json +2 -2
- package/references/orchestration/GOD-ORCHESTRATOR-RUNBOOK.md +47 -0
- package/routing/recipes/audit-remediate.yaml +30 -0
- package/skills/god-version.md +1 -1
- package/workflows/full-arc.yaml +35 -3
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Godpowers PreToolUse
|
|
3
|
-
#
|
|
4
|
-
# Warns
|
|
5
|
-
#
|
|
2
|
+
# Godpowers PreToolUse advisory hook (best-effort, NOT a security boundary).
|
|
3
|
+
#
|
|
4
|
+
# Warns before some common destructive command spellings when run inside a
|
|
5
|
+
# Godpowers project: deleting .godpowers/, git reset --hard, force push,
|
|
6
|
+
# npm publish, gh release create. It matches command text heuristically after
|
|
7
|
+
# normalizing whitespace, so it tolerates spacing and short-flag variants
|
|
8
|
+
# (rm -fr, -r -f, ./ prefix, trailing slash, push -f). It is still deliberately
|
|
9
|
+
# conservative and is easily bypassed by uncommon spellings, quoting, aliases,
|
|
10
|
+
# or a child process that does the deletion. Treat it as a typo guard that buys
|
|
11
|
+
# a confirmation prompt, not as a guarantee. See SECURITY.md.
|
|
6
12
|
|
|
7
13
|
set -euo pipefail
|
|
8
14
|
|
|
9
|
-
TOOL_NAME="${CLAUDE_TOOL_NAME:-}"
|
|
10
15
|
TOOL_INPUT="${CLAUDE_TOOL_INPUT:-}"
|
|
11
16
|
|
|
12
17
|
if [ ! -d ".godpowers" ]; then
|
|
13
18
|
exit 0
|
|
14
19
|
fi
|
|
15
20
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
21
|
+
# Collapse tabs and runs of spaces so spacing variants normalize to one form.
|
|
22
|
+
norm="$(printf '%s' "$TOOL_INPUT" | tr '\t' ' ' | tr -s ' ')"
|
|
23
|
+
|
|
24
|
+
matches() {
|
|
25
|
+
printf '%s' "$norm" | grep -Eq -- "$1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# rm targeting .godpowers (optional ./ or / prefix, optional trailing slash)
|
|
29
|
+
# that carries a recursive flag in any spelling: -rf, -fr, -r -f, -R, --recursive.
|
|
30
|
+
if matches 'rm( +-[a-zA-Z]+)* +\.?/?\.godpowers(/|$| )' && matches ' -[a-zA-Z]*[rR]|--recursive'; then
|
|
31
|
+
echo "WARNING: About to delete the .godpowers/ directory."
|
|
32
|
+
echo "This destroys all PROGRESS, PRD, ARCH, ROADMAP, and other artifacts."
|
|
33
|
+
echo "If this is intentional, confirm in chat before proceeding."
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if matches 'git +reset +--hard'; then
|
|
38
|
+
echo "WARNING: git reset --hard discards uncommitted work."
|
|
39
|
+
echo "If you have artifacts not yet committed, they will be lost."
|
|
40
|
+
echo "Consider git stash first."
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# git push with a force flag: --force, --force-with-lease, or a standalone -f.
|
|
45
|
+
if matches 'git +push +' && matches '(--force(-with-lease)?| -f( |$))'; then
|
|
46
|
+
echo "WARNING: Force pushing. If pushing to main/master, this can"
|
|
47
|
+
echo "destroy collaborators' work."
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if matches 'npm +publish'; then
|
|
52
|
+
echo "WARNING: npm publish is a public release action."
|
|
53
|
+
echo "Confirm release checklist, repo-doc-sync, repo-surface-sync,"
|
|
54
|
+
echo "release-surface-sync, package contents, and installer smoke first."
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if matches 'gh +release +create'; then
|
|
59
|
+
echo "WARNING: gh release create publishes public release notes."
|
|
60
|
+
echo "Confirm README, badges, CHANGELOG, RELEASE, package, tag, and npm version agree."
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
51
63
|
|
|
52
64
|
exit 0
|
package/lib/README.md
CHANGED
|
@@ -28,6 +28,7 @@ package-level integrations.
|
|
|
28
28
|
| `cost-tracker.js` | Track token and cost estimates from event streams. |
|
|
29
29
|
| `atomic-write.js` | Write load-bearing files through temp-file validation and atomic rename. |
|
|
30
30
|
| `fs-async.js` | Promise-based file read/write helpers for non-blocking runtime paths. |
|
|
31
|
+
| `sync-fs.js` | Shared project-relative read/write/exists/readJson helpers for the `*-sync` modules. |
|
|
31
32
|
|
|
32
33
|
## Events and observability
|
|
33
34
|
|
|
@@ -38,12 +39,17 @@ package-level integrations.
|
|
|
38
39
|
| `otel-exporter.js` | Export Godpowers events in an OpenTelemetry-shaped format. |
|
|
39
40
|
| `runtime-audit.js` | Audit runtime health and expected project state. |
|
|
40
41
|
| `runtime-test.js` | Provide runtime checks used by package tests. |
|
|
42
|
+
| `evidence.js` | Enforced producer of executed/attested verification records, the state.json rollup, gate events, reflections, memory, lessons, and outcome loops. |
|
|
43
|
+
| `evidence-import.js` | Import an existing `.mythify/` ledger into `.godpowers/ledger/`. |
|
|
44
|
+
| `work-report.js` | Render the verification play-by-play from the evidence ledger. |
|
|
45
|
+
| `adoption-metrics.js` | Derive adoption and outcome metrics from event streams. |
|
|
41
46
|
|
|
42
47
|
## Routing and execution
|
|
43
48
|
|
|
44
49
|
| Module | Purpose |
|
|
45
50
|
|--------|---------|
|
|
46
51
|
| `router.js` | Resolve user intent to skills, agents, recipes, and workflows. |
|
|
52
|
+
| `quarterback.js` | Entry router that classifies a prompt into a play and refuses new work when the project is on red. |
|
|
47
53
|
| `command-families.js` | Define UX command families, status views, decision ladders, and trigger precedence helpers. |
|
|
48
54
|
| `recipes.js` | Load and validate routing recipes. |
|
|
49
55
|
| `workflow-parser.js` | Parse workflow YAML into executable steps. |
|
|
@@ -59,7 +65,7 @@ package-level integrations.
|
|
|
59
65
|
|
|
60
66
|
| Module | Purpose |
|
|
61
67
|
|--------|---------|
|
|
62
|
-
| `artifact-map.js` |
|
|
68
|
+
| `artifact-map.js` | Tier gate artifact map: the per-tier required artifacts and state steps used by dashboards, gates, and doc-count checks. (Module-local paths stay in their owning module; `state.json` is named via `state.STATE_FILE`.) |
|
|
63
69
|
| `artifact-linter.js` | Check artifacts for required labels, evidence, and domain precision. |
|
|
64
70
|
| `artifact-diff.js` | Compare artifact changes for review and release workflows. |
|
|
65
71
|
| `gate.js` | Run executable artifact gates for Phase 1 tier completion checks. |
|
|
@@ -98,6 +104,7 @@ package-level integrations.
|
|
|
98
104
|
| `drift-detector.js` | Detect context drift between artifacts and implementation. |
|
|
99
105
|
| `impact.js` | Summarize expected impact of proposed changes. |
|
|
100
106
|
| `linkage.js` | Connect artifacts, stories, and implementation files. |
|
|
107
|
+
| `requirements.js` | Track which PRD requirements are done, in progress, or untouched from disk evidence. |
|
|
101
108
|
| `multi-repo-detector.js` | Detect multi-repository workspaces. |
|
|
102
109
|
| `reverse-sync.js` | Reflect implementation changes back into artifacts. |
|
|
103
110
|
| `review-required.js` | Decide when review gates should block progress. |
|
|
@@ -111,6 +118,9 @@ package-level integrations.
|
|
|
111
118
|
| `installer-files.js` | File-copy helpers shared by the installer and its tests. |
|
|
112
119
|
| `installer-args.js` | Parse `bin/install.js` arguments and subcommands. |
|
|
113
120
|
| `cli-dispatch.js` | Dispatch local CLI helper commands such as status, quick-proof, gate, dogfood, and extension-scaffold. |
|
|
121
|
+
| `cli-log.js` | Shared ANSI console logger (log/success/warn/error) for the binary and CLI dispatch. |
|
|
122
|
+
| `text-util.js` | Small shared string helpers (the canonical `slugify`). |
|
|
123
|
+
| `mcp-info.js` | Render read-only MCP companion setup instructions for `npx godpowers mcp-info`. |
|
|
114
124
|
| `install-profiles.js` | Select smaller role-specific slash-command install surfaces. |
|
|
115
125
|
| `surface-profile.js` | Preview and apply runtime command surface profile switches after install. |
|
|
116
126
|
| `installer-runtimes.js` | Map supported runtimes to their config directories. |
|
package/lib/artifact-map.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared tier artifact map for dashboard, gates, and documentation checks.
|
|
3
|
+
*
|
|
4
|
+
* Scope: this owns the per-tier *gate artifacts* (which files a tier requires
|
|
5
|
+
* and the state step it maps to). It is not a flat registry of every
|
|
6
|
+
* `.godpowers/...` path: module-local artifacts (a sync module's log file, the
|
|
7
|
+
* evidence ledger) live in their owning module, and `state.json` is named via
|
|
8
|
+
* `state.STATE_FILE`.
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
11
|
const TIER_ARTIFACTS = {
|
package/lib/cli-dispatch.js
CHANGED
|
@@ -5,25 +5,10 @@
|
|
|
5
5
|
const gate = require('./gate');
|
|
6
6
|
const identity = require('./package-identity');
|
|
7
7
|
const stateAdvance = require('./state-advance');
|
|
8
|
+
const { log, success, warn, error } = require('./cli-log');
|
|
8
9
|
|
|
9
10
|
const VERSION = identity.PACKAGE_VERSION;
|
|
10
11
|
|
|
11
|
-
function log(msg) {
|
|
12
|
-
console.log(` ${msg}`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function success(msg) {
|
|
16
|
-
console.log(` \x1b[32m+\x1b[0m ${msg}`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function warn(msg) {
|
|
20
|
-
console.log(` \x1b[33m!\x1b[0m ${msg}`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function error(msg) {
|
|
24
|
-
console.error(` \x1b[31mx\x1b[0m ${msg}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
12
|
function runAutomationCommand(opts) {
|
|
28
13
|
const automation = require('./automation-providers');
|
|
29
14
|
const result = opts.command === 'automation-setup'
|
|
@@ -501,7 +486,20 @@ function runOutcomeCommand(opts) {
|
|
|
501
486
|
projectRoot
|
|
502
487
|
});
|
|
503
488
|
} else if (action === 'check') {
|
|
504
|
-
payload = evidence.outcome.check(opts.outcomeSlug, {
|
|
489
|
+
payload = evidence.outcome.check(opts.outcomeSlug, {
|
|
490
|
+
projectRoot,
|
|
491
|
+
// SEC-002: announce the disk-sourced verifier before it runs, so running
|
|
492
|
+
// `outcome check` inside an untrusted cloned repo cannot silently execute
|
|
493
|
+
// a planted command. Goes to stderr so it never corrupts --json on stdout.
|
|
494
|
+
notice: ({ verifier, source }) => {
|
|
495
|
+
const rel = relLedger({ project: projectRoot }, source) || source;
|
|
496
|
+
process.stderr.write(
|
|
497
|
+
` notice: outcome '${opts.outcomeSlug}' runs a verifier loaded from ${rel}\n` +
|
|
498
|
+
` command: ${verifier}\n` +
|
|
499
|
+
` (.godpowers/ledger/ carries executable state; only run 'outcome check' in repos you trust)\n`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
505
503
|
} else if (action === 'stop') {
|
|
506
504
|
payload = evidence.outcome.stop(opts.outcomeSlug, opts.reason || undefined, { projectRoot });
|
|
507
505
|
} else {
|
|
@@ -626,11 +624,22 @@ const COMMAND_RUNNERS = {
|
|
|
626
624
|
|
|
627
625
|
function runCommand(opts) {
|
|
628
626
|
const runner = COMMAND_RUNNERS[opts.command];
|
|
629
|
-
if (runner)
|
|
627
|
+
if (!runner) return false;
|
|
628
|
+
try {
|
|
630
629
|
runner(opts);
|
|
631
|
-
|
|
630
|
+
} catch (err) {
|
|
631
|
+
// ERR-002: a corrupt state.json throws from state.read(). Surface the
|
|
632
|
+
// helpful message as a clean one-liner with a non-zero exit instead of a
|
|
633
|
+
// raw stack trace. Match the typed error code (ERR-004), not the message
|
|
634
|
+
// prose. Re-throw anything else so genuine bugs still surface.
|
|
635
|
+
if (err && err.code === 'CORRUPT_STATE') {
|
|
636
|
+
error(err.message);
|
|
637
|
+
process.exitCode = 1;
|
|
638
|
+
} else {
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
632
641
|
}
|
|
633
|
-
return
|
|
642
|
+
return true;
|
|
634
643
|
}
|
|
635
644
|
|
|
636
645
|
module.exports = {
|
package/lib/cli-log.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ANSI console logger for the installer binary and CLI dispatch (QUAL-002).
|
|
3
|
+
*
|
|
4
|
+
* bin/install.js and lib/cli-dispatch.js previously defined these four helpers
|
|
5
|
+
* verbatim. They now share one copy so the prefix/colour convention lives once.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function log(msg) {
|
|
9
|
+
console.log(` ${msg}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function success(msg) {
|
|
13
|
+
console.log(` \x1b[32m+\x1b[0m ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function warn(msg) {
|
|
17
|
+
console.log(` \x1b[33m!\x1b[0m ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function error(msg) {
|
|
21
|
+
console.error(` \x1b[31mx\x1b[0m ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { log, success, warn, error };
|
package/lib/dashboard.js
CHANGED
|
@@ -17,6 +17,7 @@ const repoDocSync = require('./repo-doc-sync');
|
|
|
17
17
|
const repoSurfaceSync = require('./repo-surface-sync');
|
|
18
18
|
const hostCapabilities = require('./host-capabilities');
|
|
19
19
|
const artifactMap = require('./artifact-map');
|
|
20
|
+
const { read: readText, exists } = require('./sync-fs');
|
|
20
21
|
|
|
21
22
|
const GOD_DIR = '.godpowers';
|
|
22
23
|
const PRD_PATH = artifactMap.requiredArtifactsForTier('prd')[0].path;
|
|
@@ -25,16 +26,6 @@ const CHECKPOINT_PATH = '.godpowers/CHECKPOINT.md';
|
|
|
25
26
|
const SYNC_LOG_PATH = '.godpowers/SYNC-LOG.md';
|
|
26
27
|
const REVIEW_PATH = '.godpowers/REVIEW-REQUIRED.md';
|
|
27
28
|
|
|
28
|
-
function exists(projectRoot, relPath) {
|
|
29
|
-
return fs.existsSync(path.join(projectRoot, relPath));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readText(projectRoot, relPath) {
|
|
33
|
-
const file = path.join(projectRoot, relPath);
|
|
34
|
-
if (!fs.existsSync(file)) return '';
|
|
35
|
-
return fs.readFileSync(file, 'utf8');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
function mtimeMs(projectRoot, relPath) {
|
|
39
30
|
const file = path.join(projectRoot, relPath);
|
|
40
31
|
if (!fs.existsSync(file)) return null;
|
package/lib/evidence.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* Adaptations from the upstream Node engine (see .provenance.json):
|
|
11
11
|
* - Mythify's plan/step context becomes Godpowers' arc/substep context.
|
|
12
12
|
* - The .mythify/ state dir becomes .godpowers/ledger/.
|
|
13
|
-
* - The jsonl append
|
|
14
|
-
* torn
|
|
13
|
+
* - The jsonl append uses O_APPEND (fs.appendFileSync) so concurrent writers
|
|
14
|
+
* never clobber each other's records; a torn line from an interleaved large
|
|
15
|
+
* write is tolerated by the reader, which skips unparseable records.
|
|
15
16
|
*
|
|
16
17
|
* What this adds on top of the upstream engine (the Godpowers integration):
|
|
17
18
|
* 1. .godpowers/ledger/verifications.jsonl: append-only, Mythify-shape record,
|
|
@@ -61,6 +62,9 @@ const events = require('./events');
|
|
|
61
62
|
const TAIL_CHARS = 4000;
|
|
62
63
|
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
63
64
|
const DIAGNOSTICS_LIMIT = 1000;
|
|
65
|
+
// Cap on captured stdout/stderr per verify command. Exceeding it makes
|
|
66
|
+
// spawnSync raise ENOBUFS and truncate output, so the verdict is unreliable.
|
|
67
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
64
68
|
|
|
65
69
|
// Substeps whose close gate requires an executed, verified:true record (the
|
|
66
70
|
// runtime/executable-gated tiers). Other substeps (planning, repo, observe,
|
|
@@ -161,19 +165,33 @@ function slugify(text) {
|
|
|
161
165
|
.slice(0, 40);
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
// SEC-003: mask obvious secret shapes before echoing a command into the
|
|
169
|
+
// human-readable LEDGER-LOG.md. The durable verifications.jsonl record keeps the
|
|
170
|
+
// exact command (it is the audit source of truth); this only protects the log
|
|
171
|
+
// echo. Output tails can still carry secrets, so SECURITY.md documents that
|
|
172
|
+
// .godpowers/ledger/ may capture sensitive output.
|
|
173
|
+
function redactSecrets(text) {
|
|
174
|
+
return String(text == null ? '' : text)
|
|
175
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, 'gh*_***REDACTED***')
|
|
176
|
+
.replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, 'sk-***REDACTED***')
|
|
177
|
+
.replace(/\bAKIA[0-9A-Z]{16}\b/g, 'AKIA***REDACTED***')
|
|
178
|
+
.replace(/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, 'xox*-***REDACTED***')
|
|
179
|
+
.replace(/(--?(?:token|password|passwd|secret|api[-_]?key)[=\s])\S+/gi, '$1***REDACTED***');
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
// ---------------------------------------------------------------------------
|
|
165
|
-
// Ledger append (
|
|
183
|
+
// Ledger append (O_APPEND) and tolerant read
|
|
166
184
|
// ---------------------------------------------------------------------------
|
|
167
185
|
|
|
168
186
|
function appendJsonlAtomic(file, record) {
|
|
169
187
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
// O_APPEND: each record is written at EOF in a single positioned write, so
|
|
189
|
+
// two concurrent writers never overwrite each other's record. The previous
|
|
190
|
+
// read-concat-rewrite was last-writer-wins (it lost records under concurrent
|
|
191
|
+
// invocation) and rewrote the whole file on every append (O(n) per record).
|
|
192
|
+
// A torn line from an interleaved oversized write is tolerated by readJsonl,
|
|
193
|
+
// which skips unparseable records. Mirrors the append in lib/events.js.
|
|
194
|
+
fs.appendFileSync(file, JSON.stringify(record) + '\n');
|
|
177
195
|
return file;
|
|
178
196
|
}
|
|
179
197
|
|
|
@@ -255,18 +273,27 @@ function runCommand(command, timeoutSeconds) {
|
|
|
255
273
|
shell: true,
|
|
256
274
|
encoding: 'utf8',
|
|
257
275
|
timeout: Math.round(timeoutSeconds * 1000),
|
|
258
|
-
maxBuffer:
|
|
276
|
+
maxBuffer: MAX_OUTPUT_BYTES
|
|
259
277
|
});
|
|
260
278
|
const durationSeconds = Number(process.hrtime.bigint() - startedAt) / 1e9;
|
|
261
279
|
let stdoutTail = tail(run.stdout);
|
|
262
280
|
let stderrTail = tail(run.stderr);
|
|
263
281
|
const timedOut = Boolean(run.error && run.error.code === 'ETIMEDOUT');
|
|
282
|
+
const bufferOverflow = Boolean(run.error && run.error.code === 'ENOBUFS');
|
|
264
283
|
let exitCode;
|
|
265
284
|
let verified;
|
|
266
285
|
if (timedOut) {
|
|
267
286
|
exitCode = -1;
|
|
268
287
|
verified = false;
|
|
269
288
|
stderrTail = stderrTail + (stderrTail ? '\n' : '') + `(timed out after ${timeoutSeconds} seconds)`;
|
|
289
|
+
} else if (bufferOverflow) {
|
|
290
|
+
// Output exceeded the capture buffer: spawnSync truncated stdout/stderr, so
|
|
291
|
+
// we cannot trust the exit status. Surface this distinctly rather than
|
|
292
|
+
// folding it into a plain command failure.
|
|
293
|
+
exitCode = -1;
|
|
294
|
+
verified = false;
|
|
295
|
+
stderrTail = stderrTail + (stderrTail ? '\n' : '') +
|
|
296
|
+
`(output exceeded ${Math.round(MAX_OUTPUT_BYTES / (1024 * 1024))} MB buffer; output truncated and verdict unreliable)`;
|
|
270
297
|
} else if (typeof run.status === 'number') {
|
|
271
298
|
exitCode = run.status;
|
|
272
299
|
verified = exitCode === 0;
|
|
@@ -280,7 +307,7 @@ function runCommand(command, timeoutSeconds) {
|
|
|
280
307
|
: 'command did not produce an exit code';
|
|
281
308
|
stderrTail = stderrTail + (stderrTail ? '\n' : '') + `(${reason})`;
|
|
282
309
|
}
|
|
283
|
-
return { exitCode, verified, durationSeconds, stdoutTail, stderrTail, timedOut };
|
|
310
|
+
return { exitCode, verified, durationSeconds, stdoutTail, stderrTail, timedOut, bufferOverflow };
|
|
284
311
|
}
|
|
285
312
|
|
|
286
313
|
// ---------------------------------------------------------------------------
|
|
@@ -449,7 +476,7 @@ function verify(command, opts = {}) {
|
|
|
449
476
|
appendRecord(projectRoot, record);
|
|
450
477
|
appendLog(
|
|
451
478
|
projectRoot,
|
|
452
|
-
`verify ${record.verified ? 'PASS' : 'FAIL'} substep=${context.substep || '-'} exit=${record.exit_code} cmd=\`${command}\``
|
|
479
|
+
`verify ${record.verified ? 'PASS' : 'FAIL'} substep=${context.substep || '-'} exit=${record.exit_code} cmd=\`${redactSecrets(command)}\``
|
|
453
480
|
);
|
|
454
481
|
|
|
455
482
|
const rollup = rollUp(projectRoot, opts.substep, record);
|
|
@@ -818,6 +845,15 @@ function outcomeCheck(name, opts = {}) {
|
|
|
818
845
|
if (goal.status !== 'active') return { ran: false, reason: `outcome-${goal.status}`, goal };
|
|
819
846
|
if (!goal.verifier) return { ran: false, reason: 'no-verifier', goal };
|
|
820
847
|
|
|
848
|
+
// SEC-002: the verifier is a shell command read from goal.json on disk, not
|
|
849
|
+
// from a live flag, so `outcome check` in a cloned untrusted repo would
|
|
850
|
+
// otherwise execute a planted command silently. Surface what is about to run
|
|
851
|
+
// and where it came from before executing. The notice is informational; the
|
|
852
|
+
// CLI passes one that prints to stderr.
|
|
853
|
+
if (typeof opts.notice === 'function') {
|
|
854
|
+
opts.notice({ verifier: goal.verifier, source: outcomeGoalPath(projectRoot, slug) });
|
|
855
|
+
}
|
|
856
|
+
|
|
821
857
|
const result = verify(goal.verifier, {
|
|
822
858
|
substep: goal.substep || undefined,
|
|
823
859
|
claim: goal.title,
|
|
@@ -901,6 +937,7 @@ module.exports = {
|
|
|
901
937
|
DEFAULT_TIMEOUT_SECONDS,
|
|
902
938
|
// Internals exposed for tests and the re-sync script.
|
|
903
939
|
_runCommand: runCommand,
|
|
940
|
+
_redactSecrets: redactSecrets,
|
|
904
941
|
_toStateCommand: toStateCommand,
|
|
905
942
|
_substepContext: substepContext,
|
|
906
943
|
_rollUp: rollUp,
|
package/lib/gate.js
CHANGED
|
@@ -215,7 +215,7 @@ function checkStateStepEvidence(projectRoot, tier, result) {
|
|
|
215
215
|
const stepRef = artifactMap.stateStepForTier(tier);
|
|
216
216
|
if (!stepRef) return null;
|
|
217
217
|
|
|
218
|
-
const relPath =
|
|
218
|
+
const relPath = stateStore.STATE_FILE;
|
|
219
219
|
const currentState = stateStore.read(projectRoot);
|
|
220
220
|
if (!currentState) {
|
|
221
221
|
const finding = makeFinding(
|
|
@@ -275,7 +275,7 @@ function checkStateStepEvidence(projectRoot, tier, result) {
|
|
|
275
275
|
// tier-prefixed so the build tier keeps its existing `build-verification-*`
|
|
276
276
|
// contract while harden gains `harden-verification-*`.
|
|
277
277
|
function checkExecutedEvidence(result, step, tier) {
|
|
278
|
-
const relPath =
|
|
278
|
+
const relPath = stateStore.STATE_FILE;
|
|
279
279
|
if (!step) return;
|
|
280
280
|
const label = tier.charAt(0).toUpperCase() + tier.slice(1);
|
|
281
281
|
const failedCommands = commandsWithStatus(step, 'fail');
|