session-orchestrator 5.2.0 → 5.3.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/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Stdin-only boundary for commands/session.md; never executes task context. */
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { resolveSessionInvocation } from './lib/session-invocation.mjs';
|
|
6
|
+
|
|
7
|
+
const HELP = `Usage: node scripts/resolve-session-invocation.mjs [--json] < arguments.txt
|
|
8
|
+
|
|
9
|
+
Read the complete /session argument text from UTF-8 stdin.
|
|
10
|
+
Resolve the leading housekeeping, feature, deep or ultradeep token; preserve
|
|
11
|
+
the rest as task context. Empty input defaults to deep. An invalid mode emits
|
|
12
|
+
a warning and resolves to deep, as the session command has always specified.
|
|
13
|
+
|
|
14
|
+
--json Emit one JSON object instead of a human-readable summary
|
|
15
|
+
--help Show this help
|
|
16
|
+
--version Show the plugin version
|
|
17
|
+
|
|
18
|
+
Example: node scripts/resolve-session-invocation.mjs --json < arguments.txt
|
|
19
|
+
Write arguments.txt with a file tool; do not interpolate user text into a shell.
|
|
20
|
+
Exit codes: 0 resolved (including fallback), 1 bad CLI arguments, 2 stdin I/O error
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
let options;
|
|
24
|
+
try {
|
|
25
|
+
options = parseArgs({
|
|
26
|
+
options: { json: { type: 'boolean' }, help: { type: 'boolean' }, version: { type: 'boolean' } },
|
|
27
|
+
allowPositionals: false,
|
|
28
|
+
}).values;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
process.stderr.write(`session-invocation: ${error.message}\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.help) {
|
|
35
|
+
process.stdout.write(HELP);
|
|
36
|
+
} else if (options.version) {
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
`${JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version}\n`,
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
let input;
|
|
42
|
+
try {
|
|
43
|
+
input = readFileSync(0, 'utf8');
|
|
44
|
+
} catch {
|
|
45
|
+
process.stderr.write('session-invocation: could not read UTF-8 stdin\n');
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
const resolved = resolveSessionInvocation(input);
|
|
49
|
+
if (resolved.invalidMode !== undefined) {
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
`Invalid session type '${resolved.invalidMode}'. Valid types: housekeeping, feature, deep (alias: ultradeep). Falling back to deep.\n`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
process.stdout.write(
|
|
55
|
+
options.json
|
|
56
|
+
? `${JSON.stringify(resolved)}\n`
|
|
57
|
+
: `Session type: ${resolved.sessionType}${resolved.profile ? ` (profile: ${resolved.profile})` : ''}\nTask context: ${resolved.context}\n`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -23,9 +23,15 @@
|
|
|
23
23
|
* 0 — pass (or informational; non-blocking variants always exit 0)
|
|
24
24
|
* 1 — script error (bad arguments, missing dependencies)
|
|
25
25
|
* 2 — gate failed (full-gate only: typecheck/test/lint errors)
|
|
26
|
+
* 124 — the gate sub-script exceeded the wall-clock ceiling and its whole
|
|
27
|
+
* process GROUP was killed (SIGTERM→SIGKILL). Same value coreutils
|
|
28
|
+
* `timeout(1)` uses. Applies to EVERY variant, including the otherwise
|
|
29
|
+
* non-blocking ones: a killed gate measured nothing, so reporting it as
|
|
30
|
+
* 0 would be a false green (Epic #1425 A3).
|
|
26
31
|
*
|
|
27
32
|
* The gate sub-scripts in scripts/lib/gates/ are NOT reimplemented here; they are
|
|
28
|
-
* invoked via
|
|
33
|
+
* invoked via spawnInGroup() — a detached, process-group-leading shell — with the
|
|
34
|
+
* required env vars.
|
|
29
35
|
*
|
|
30
36
|
* References:
|
|
31
37
|
* scripts/run-quality-gate.sh — original shell orchestrator
|
|
@@ -37,12 +43,18 @@
|
|
|
37
43
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
38
44
|
import { join, dirname, resolve } from 'node:path';
|
|
39
45
|
import { fileURLToPath } from 'node:url';
|
|
40
|
-
import {
|
|
46
|
+
import { spawn } from 'node:child_process';
|
|
41
47
|
|
|
42
48
|
import { die, warn } from './lib/common.mjs';
|
|
43
49
|
import { loadQualityGatesPolicy, resolveCommand } from './lib/quality-gates-policy.mjs';
|
|
44
50
|
import { emitEvent, sessionAttribution } from './lib/events.mjs';
|
|
45
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
admitSuiteCounts,
|
|
53
|
+
publishGateOutcome,
|
|
54
|
+
resolveGateTimeoutMs,
|
|
55
|
+
} from './lib/gates/gate-helpers.mjs';
|
|
56
|
+
import { buildCommandSignature, spawnInGroup } from './lib/process-group.mjs';
|
|
57
|
+
import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
|
|
46
58
|
import { findScopeFile } from './lib/scope-gate.mjs';
|
|
47
59
|
|
|
48
60
|
// ---------------------------------------------------------------------------
|
|
@@ -62,6 +74,24 @@ const VALID_VARIANTS = ['baseline', 'incremental', 'full-gate', 'per-file'];
|
|
|
62
74
|
*/
|
|
63
75
|
const GATE_STDOUT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
64
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Head-room between the ceiling the gate's OWN commands run under
|
|
79
|
+
* (`resolveGateTimeoutMs()`, applied per command inside `runCheck`) and the one
|
|
80
|
+
* this wrapper applies to the gate sub-script as a whole.
|
|
81
|
+
*
|
|
82
|
+
* The inner cap must fire FIRST: it kills exactly the wedged command and still
|
|
83
|
+
* lets the gate print its JSON envelope, name the failing check on stderr and
|
|
84
|
+
* exit 2. This outer cap is the backstop for the case the inner one cannot
|
|
85
|
+
* reach — a gate wedged outside `runCheck`, or a kill ladder that left a
|
|
86
|
+
* survivor holding the pipe open.
|
|
87
|
+
*
|
|
88
|
+
* Named ceiling (BV-004): 60 s. It only has to cover one full SIGTERM→grace→
|
|
89
|
+
* SIGKILL→verify ladder (`DEFAULT_KILL_GRACE_MS` 10 s + 0.5 s verify + 1 s
|
|
90
|
+
* deadline slack ≈ 11.5 s) plus the gate's own JSON write. Revisit if the grace
|
|
91
|
+
* period is ever raised past ~45 s.
|
|
92
|
+
*/
|
|
93
|
+
const GATE_OUTER_TIMEOUT_RESERVE_MS = 60_000;
|
|
94
|
+
|
|
65
95
|
const DEFAULT_TEST_CMD = 'npm test';
|
|
66
96
|
const DEFAULT_TYPECHECK_CMD = 'npm run typecheck';
|
|
67
97
|
const DEFAULT_LINT_CMD = 'npm run lint';
|
|
@@ -88,7 +118,8 @@ if (argv.includes('-h') || argv.includes('--help')) {
|
|
|
88
118
|
'Exit codes:\n' +
|
|
89
119
|
' 0 — pass (non-blocking variants always exit 0)\n' +
|
|
90
120
|
' 1 — script error (bad arguments, missing dependencies)\n' +
|
|
91
|
-
' 2 — gate failed (full-gate only)\n'
|
|
121
|
+
' 2 — gate failed (full-gate only)\n' +
|
|
122
|
+
' 124 — gate timed out; its process group was killed (SIGTERM→SIGKILL)\n',
|
|
92
123
|
);
|
|
93
124
|
process.exit(0);
|
|
94
125
|
}
|
|
@@ -355,6 +386,11 @@ if (!existsSync(gatePath)) {
|
|
|
355
386
|
die(`Gate script not found: ${gatePath}`);
|
|
356
387
|
}
|
|
357
388
|
|
|
389
|
+
// Resolved BEFORE the spawn (it used to sit beside the telemetry block below):
|
|
390
|
+
// the gate-process ledger the spawn writes is pinned to the same root as the
|
|
391
|
+
// event, for the same reason — see the spawn's `repoRoot` comment.
|
|
392
|
+
const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
|
|
393
|
+
|
|
358
394
|
// `npm_config_loglevel` is INHERITED by every descendant, and the pre-push hook
|
|
359
395
|
// invokes this script as `npm run --silent quality-gate` — which sets it to
|
|
360
396
|
// `silent`. That level then reached the gate's own children: `npm pack
|
|
@@ -368,37 +404,141 @@ if (!existsSync(gatePath)) {
|
|
|
368
404
|
// independent of how the gate itself was invoked. `--silent` still does its real
|
|
369
405
|
// job — keeping THIS process's stdout to the single JSON envelope — because the
|
|
370
406
|
// children's output is captured by `runCheck`, never streamed.
|
|
407
|
+
/**
|
|
408
|
+
* Per-command wall-clock ceiling for PATH B (#1425 A3 / #1432).
|
|
409
|
+
*
|
|
410
|
+
* Precedence, highest first:
|
|
411
|
+
* 1. `SO_GATE_TIMEOUT_MS` — the OPERATOR override. It is inherited by every
|
|
412
|
+
* descendant, so a gate child that re-derives the ceiling gets the same
|
|
413
|
+
* answer; `resolveGateTimeoutMs()` already reads it.
|
|
414
|
+
* 2. Session Config `gate.timeout-path-b-ms` — the COMMITTED default.
|
|
415
|
+
* 3. `DEFAULT_GATE_TIMEOUT_MS` (900 000), via `resolveGateTimeoutMs()`.
|
|
416
|
+
*
|
|
417
|
+
* Published to the gate sub-script as `GATE_TIMEOUT_MS` — a DIFFERENT name from
|
|
418
|
+
* the operator override on purpose: writing the resolved value back into
|
|
419
|
+
* `SO_GATE_TIMEOUT_MS` would make a committed default indistinguishable from an
|
|
420
|
+
* operator decision for every process further down the tree.
|
|
421
|
+
*/
|
|
422
|
+
const configuredGateTimeoutMs = (() => {
|
|
423
|
+
const block = configJson !== null && typeof configJson === 'object' ? configJson.gate : null;
|
|
424
|
+
const raw = block && typeof block === 'object' ? block['timeout-path-b-ms'] : undefined;
|
|
425
|
+
return Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
426
|
+
})();
|
|
427
|
+
const operatorTimeoutOverride = (process.env.SO_GATE_TIMEOUT_MS || '').trim() !== '';
|
|
428
|
+
const commandTimeoutMs = operatorTimeoutOverride || configuredGateTimeoutMs === null
|
|
429
|
+
? resolveGateTimeoutMs()
|
|
430
|
+
: configuredGateTimeoutMs;
|
|
431
|
+
|
|
432
|
+
// Gate-process REGISTER root — the same precedence the telemetry destination
|
|
433
|
+
// uses below (`--ledger-root` > project-dir env > cwd), so a sandboxed test that
|
|
434
|
+
// only sets CLAUDE_PROJECT_DIR never writes register lines into the checkout.
|
|
435
|
+
const gateLedgerRoot =
|
|
436
|
+
ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot;
|
|
437
|
+
|
|
371
438
|
const env = {
|
|
372
439
|
...process.env,
|
|
373
440
|
npm_config_loglevel: 'notice',
|
|
374
441
|
TYPECHECK_CMD,
|
|
375
442
|
TEST_CMD,
|
|
376
443
|
LINT_CMD,
|
|
444
|
+
GATE_TIMEOUT_MS: String(commandTimeoutMs),
|
|
445
|
+
// Same resolution the wrapper uses for its OWN spawn (`--ledger-root` > repo
|
|
446
|
+
// root): the gate sub-scripts pass it to runCheck() so every register line of
|
|
447
|
+
// one gate run lands in ONE ledger (#1425 A4, W5 fix-pass).
|
|
448
|
+
GATE_LEDGER_ROOT: gateLedgerRoot,
|
|
377
449
|
FILES: files,
|
|
378
450
|
SESSION_START_REF: sessionStartRef,
|
|
379
451
|
};
|
|
380
452
|
|
|
453
|
+
/**
|
|
454
|
+
* POSIX single-quote one argument for the shell `spawnInGroup` runs the command
|
|
455
|
+
* through. The gate path is derived from `import.meta.url`, so it carries
|
|
456
|
+
* whatever the checkout path carries — a space in it must not split the command.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} value
|
|
459
|
+
* @returns {string}
|
|
460
|
+
*/
|
|
461
|
+
function shellQuote(value) {
|
|
462
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// The gate sub-script runs as the LEADER OF ITS OWN PROCESS GROUP under a
|
|
466
|
+
// wall-clock ceiling (Epic #1425 A3). Before this, `spawnSync('node', [gatePath])`
|
|
467
|
+
// had NO timeout at all and no group semantics: a wedged `tsgo` or vitest worker
|
|
468
|
+
// two levels down was reparented to PPID 1 and kept its memory (2026-09-20: four
|
|
469
|
+
// such orphans, up to 8.0 GB RSS each, host at 13 % free).
|
|
470
|
+
//
|
|
381
471
|
// stdout is PIPED (not inherited) so the suite counts the gate already computed
|
|
382
472
|
// can be lifted straight off its JSON envelope into telemetry (#954) instead of
|
|
383
473
|
// travelling as prose through the STATE.md header. The envelope is re-emitted
|
|
384
474
|
// verbatim below, so the stdout contract is unchanged — a gate sub-script writes
|
|
385
475
|
// exactly one JSON line at the very end (its own child commands are captured by
|
|
386
|
-
// `runCheck`), so nothing streamed before and nothing streams now.
|
|
387
|
-
//
|
|
388
|
-
|
|
476
|
+
// `runCheck`), so nothing streamed before and nothing streams now.
|
|
477
|
+
//
|
|
478
|
+
// stderr stays INHERITED, and that is why `spawnFn` is overridden here rather
|
|
479
|
+
// than left at its default: `spawnInGroup` merges stdout and stderr into one
|
|
480
|
+
// capture, which would interleave the gate's failure disclosure (hundreds of
|
|
481
|
+
// lines, #1149) into the single JSON document every consumer parses off stdout.
|
|
482
|
+
// Handing it a child with `stderr: 'inherit'` leaves `child.stderr` null, the
|
|
483
|
+
// module's own `child.stderr?.on(…)` a no-op, and the gate's warnings live on
|
|
484
|
+
// the operator's terminal exactly as before.
|
|
485
|
+
const gateCommand = `node ${shellQuote(gatePath)}`;
|
|
486
|
+
// Derived from the SAME resolved per-command ceiling published as
|
|
487
|
+
// `GATE_TIMEOUT_MS` above — not from `resolveGateTimeoutMs()` a second time.
|
|
488
|
+
// The outer cap must stay strictly ABOVE the inner one so the inner kill fires
|
|
489
|
+
// first and the gate can still print its envelope; re-deriving here would leave
|
|
490
|
+
// the outer cap at the 900 000 default while a committed
|
|
491
|
+
// `gate.timeout-path-b-ms` above it made the inner cap the later of the two.
|
|
492
|
+
const gateTimeoutMs = commandTimeoutMs + GATE_OUTER_TIMEOUT_RESERVE_MS;
|
|
493
|
+
const result = await spawnInGroup(gateCommand, {
|
|
494
|
+
cwd: repoRoot,
|
|
389
495
|
env,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
496
|
+
timeoutMs: gateTimeoutMs,
|
|
497
|
+
maxOutputBytes: GATE_STDOUT_MAX_BUFFER_BYTES,
|
|
498
|
+
// The ledger is what the orphan reaper (#1425 B) reads to tell its OWN gate
|
|
499
|
+
// processes from every other `node` on the host, so it is pinned to the same
|
|
500
|
+
// root the telemetry is — under the pre-push hook the tree the gate runs in
|
|
501
|
+
// is deleted seconds later, and the record with it.
|
|
502
|
+
repoRoot: gateLedgerRoot,
|
|
503
|
+
commandSignature: buildCommandSignature(gateCommand),
|
|
504
|
+
// The OWNER the ledger records, from the PROCESS-LOCAL witness only — the same
|
|
505
|
+
// rule `scripts/lib/quality-gate.mjs` follows for the gate commands it spawns
|
|
506
|
+
// (`.claude/rules/identity-and-locks.md`: rank witnesses, never union them).
|
|
507
|
+
// `.orchestrator/session.lock` is deliberately NOT consulted: it is a
|
|
508
|
+
// repo-GLOBAL artefact any session in this working copy may hold, so reading
|
|
509
|
+
// it would stamp a PEER's id onto our own gate process and point the orphan
|
|
510
|
+
// reaper (#1425 B) at the wrong owner. No witness → `null`, which the ledger
|
|
511
|
+
// records as "owner unknown" — the state every record carried before this.
|
|
512
|
+
sessionId: [...readProcessLocalSessionIds({ env: process.env, hookInput: null })][0] ?? null,
|
|
513
|
+
// stdin is IGNORED, not inherited. A gate sub-script reads no stdin, but a
|
|
514
|
+
// DETACHED child is in its own process group and is therefore not the
|
|
515
|
+
// terminal's foreground group: the first read from an inherited TTY earns it
|
|
516
|
+
// SIGTTIN, which stops the whole group until the outer ceiling kills it. An
|
|
517
|
+
// ignored stdin turns that hang into an immediate EOF.
|
|
518
|
+
spawnFn: (command, options) => spawn(command, { ...options, stdio: ['ignore', 'pipe', 'inherit'] }),
|
|
393
519
|
});
|
|
394
520
|
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
if (result.error && typeof result.status !== 'number') {
|
|
399
|
-
die(`Failed to run gate script: ${result.error.message}`);
|
|
521
|
+
// `pid: -1` is `spawnInGroup`'s spawn-failure channel (it never rejects).
|
|
522
|
+
if (result.pid === -1) {
|
|
523
|
+
die(`Failed to run gate script: ${result.fullOutput.trim()}`);
|
|
400
524
|
}
|
|
401
525
|
|
|
526
|
+
// On timeout the child was KILLED before it could write its envelope, so its
|
|
527
|
+
// capture is at best a partial JSON document. Publishing that would hand every
|
|
528
|
+
// stdout consumer a parse error where a named failure belongs; publishing the
|
|
529
|
+
// partial text AND an envelope would break the one-document contract. So the
|
|
530
|
+
// capture goes to stderr, where the operator can still read it, and stdout
|
|
531
|
+
// carries a complete `gate-timeout` envelope instead.
|
|
532
|
+
//
|
|
533
|
+
// The DECISION lives in `publishGateOutcome` (gate-helpers.mjs) and only the
|
|
534
|
+
// WRITES live here: this branch needs a real 16-minute gate to reach, so while
|
|
535
|
+
// the decision was inline it was pinned by nothing.
|
|
536
|
+
const outcome = publishGateOutcome({ result, variant, timeoutMs: gateTimeoutMs });
|
|
537
|
+
const gateStdout = result.timedOut ? '' : result.fullOutput;
|
|
538
|
+
if (outcome.stderr) process.stderr.write(outcome.stderr);
|
|
539
|
+
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
540
|
+
for (const line of outcome.warnings) warn(line);
|
|
541
|
+
|
|
402
542
|
// Quality-gate telemetry — one canonical event per gate run via emitEvent
|
|
403
543
|
// (single emission path). `sessionAttribution` is the shared helper in
|
|
404
544
|
// events.mjs (#941); this CLI wrapper runs against the CWD `repoRoot`, so the
|
|
@@ -433,8 +573,7 @@ if (result.error && typeof result.status !== 'number') {
|
|
|
433
573
|
//
|
|
434
574
|
// Best-effort: a telemetry failure must NEVER alter the gate's authoritative
|
|
435
575
|
// exit code — which is why the counts parse also lives inside this try.
|
|
436
|
-
const exitCode =
|
|
437
|
-
const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
|
|
576
|
+
const exitCode = outcome.exitCode;
|
|
438
577
|
try {
|
|
439
578
|
const counts = suiteCountsFromGateStdout(gateStdout);
|
|
440
579
|
// The names behind `counts.failed`. Absent, never `[]` — see
|
|
@@ -115,15 +115,25 @@ export function parseArgs(argv) {
|
|
|
115
115
|
return args;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Roll the plan up into the summary both output modes render.
|
|
120
|
+
*
|
|
121
|
+
* `header_raises` is additive (#1388 P7): a header-raise is an
|
|
122
|
+
* `action: 'rewrite'` plan carrying `reason: 'header-raise'`, so it was counted
|
|
123
|
+
* inside `rewrites` and invisible on the human stdout line although `--json`
|
|
124
|
+
* always carried `plans[].reason`. It is a SUBSET of `rewrites`, never a
|
|
125
|
+
* sibling category — the two are deliberately not disjoint.
|
|
126
|
+
*/
|
|
127
|
+
export function summarize(plan, args, applied) {
|
|
120
128
|
const counts = { rewrite: 0, delete: 0, keep: 0 };
|
|
121
129
|
for (const p of plan.plans) counts[p.action] = (counts[p.action] ?? 0) + 1;
|
|
130
|
+
const headerRaises = plan.plans.filter((p) => p.reason === 'header-raise').length;
|
|
122
131
|
return {
|
|
123
132
|
dry_run: args.dryRun,
|
|
124
133
|
grace_days: args.graceDays,
|
|
125
134
|
files_scanned: plan.plans.length,
|
|
126
135
|
rewrites: counts.rewrite,
|
|
136
|
+
header_raises: headerRaises,
|
|
127
137
|
deletes: counts.delete,
|
|
128
138
|
keeps: counts.keep,
|
|
129
139
|
expired_entries: plan.plans.reduce((n, p) => n + p.expiredPairIds.length, 0),
|
|
@@ -163,7 +173,8 @@ async function main(argv) {
|
|
|
163
173
|
else {
|
|
164
174
|
process.stdout.write(
|
|
165
175
|
`sweep-expired-rules: ${args.dryRun ? 'dry-run' : 'applied'} — ` +
|
|
166
|
-
`${summary.files_scanned} generated rule file(s), ${summary.rewrites} rewrite(s)
|
|
176
|
+
`${summary.files_scanned} generated rule file(s), ${summary.rewrites} rewrite(s) ` +
|
|
177
|
+
`(${summary.header_raises} header-raise(s)), ` +
|
|
167
178
|
`${summary.deletes} delete(s), ${summary.expired_entries} expired entr(ies), ` +
|
|
168
179
|
`${summary.unresolved_pairs} unresolved pair(s), ${summary.skipped.length} skipped, ` +
|
|
169
180
|
`${summary.malformed_lines} malformed learnings line(s)\n`,
|
|
@@ -433,6 +433,18 @@ if (runCheck('check-dead-bridge.mjs') !== 0) checkFailed = 1;
|
|
|
433
433
|
process.stdout.write('\n');
|
|
434
434
|
if (runCheck('check-entry-guard.mjs') !== 0) checkFailed = 1;
|
|
435
435
|
|
|
436
|
+
// BLOCKING (#1422), the complement of the check above: `check-entry-guard`
|
|
437
|
+
// judges the FORM of a guard that already exists, so a registered hook with NO
|
|
438
|
+
// guard at all passes it trivially — the #1393 regression class exactly. This
|
|
439
|
+
// one asks the other half of the question over the union of the four platform
|
|
440
|
+
// manifests: does every registered hook run its entry only under
|
|
441
|
+
// `isMainModule(import.meta.url)` / `invokedAsScript()`, and does no profile
|
|
442
|
+
// gate sit at module top level where its `process.exit(0)` kills a mere
|
|
443
|
+
// importer. Blocking honestly: 27/27 registered hooks were guarded at the
|
|
444
|
+
// moment it landed, because #1393 drained the backlog first.
|
|
445
|
+
process.stdout.write('\n');
|
|
446
|
+
if (runCheck('check-hook-entry-guards.mjs') !== 0) checkFailed = 1;
|
|
447
|
+
|
|
436
448
|
// FAIL-capable, unlike the WARN-only censuses below: R2+R4 were measured at
|
|
437
449
|
// 1 hit / 1 TP / 0 FP each against the full test corpus, so this check cannot be
|
|
438
450
|
// red on arrival for anything but a real instance of the class. The alternatives
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
* 2 — I/O error (file not found, unreadable stdin, unreadable sidecar file)
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
|
-
import path from 'node:path';
|
|
73
72
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
74
73
|
import { execFileSync } from 'node:child_process';
|
|
75
74
|
import { warn } from './lib/common.mjs';
|
|
@@ -86,6 +85,17 @@ import {
|
|
|
86
85
|
// which roles are allowed to grant zero paths.
|
|
87
86
|
isReadOnlyWaveRole,
|
|
88
87
|
READ_ONLY_WAVE_ROLES,
|
|
88
|
+
// #1398 cond. 4 / #1405 / #1406 — THE grading predicate for an absolute Gate 5b
|
|
89
|
+
// grant. Lives in scope-gate.mjs so this CLI and `hooks/enforce-scope.mjs`
|
|
90
|
+
// grade one and the same way (BV-003): before the move, only this CLI could
|
|
91
|
+
// grade at all, so the hook honoured grants the CLI would have refused and a
|
|
92
|
+
// manifest that skipped the CLI was never graded.
|
|
93
|
+
gradeScopeEntry,
|
|
94
|
+
// #1398 cond. 4 — the `resolve` injection the predicate above takes. Moved
|
|
95
|
+
// into scope-gate.mjs (from a private copy here) once the hot-path cost was
|
|
96
|
+
// measured rather than assumed, so this CLI and `hooks/enforce-scope.mjs` pass
|
|
97
|
+
// the SAME resolver and can no longer reach different verdicts for one grant.
|
|
98
|
+
canonicalizeGrantPrefix,
|
|
89
99
|
// Aliased: `expandTestSiblings` is ALSO the name of the pre-existing
|
|
90
100
|
// boolean parameter threaded through validate()/assertSubsetOrDie for the
|
|
91
101
|
// #970 flag. Aliasing the import avoids shadowing that parameter rather than
|
|
@@ -338,79 +348,6 @@ function validateSessionBinding(obj, errors, warnings) {
|
|
|
338
348
|
}
|
|
339
349
|
}
|
|
340
350
|
|
|
341
|
-
/**
|
|
342
|
-
* Literal filesystem-root forms — POSIX "/" and the Windows equivalents "\"
|
|
343
|
-
* and a bare drive root ("C:\", "C:\\", ...). Checked independently of
|
|
344
|
-
* `path.isAbsolute()` because that primitive is platform-native: on a POSIX
|
|
345
|
-
* host (this repo's dev/CI hosts) it never reports `C:\` as absolute, so a
|
|
346
|
-
* Windows-literal-root entry would otherwise slip past every check below.
|
|
347
|
-
* @type {ReadonlySet<string>}
|
|
348
|
-
*/
|
|
349
|
-
const FILESYSTEM_ROOT_LITERALS = new Set(['/', '\\']);
|
|
350
|
-
const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:\\+$/;
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* @param {string} entry
|
|
354
|
-
* @returns {boolean}
|
|
355
|
-
*/
|
|
356
|
-
function isFilesystemRootLiteral(entry) {
|
|
357
|
-
return FILESYSTEM_ROOT_LITERALS.has(entry) || WINDOWS_DRIVE_ROOT_RE.test(entry);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Well-known top-level system/home directories. A FIXED DENYLIST (not a
|
|
362
|
-
* segment-count threshold): the #792 legitimate grant
|
|
363
|
-
* (`/private/tmp/<session>/scratchpad/**`) is itself a single-segment-deep
|
|
364
|
-
* grant under an unusual root ("private"), so any segment-count heuristic
|
|
365
|
-
* tight enough to catch `/etc/**` risks catching that too, or must be tuned
|
|
366
|
-
* loosely enough to leave a gap. A fixed list of the directories a
|
|
367
|
-
* hallucinated/mis-copied wave-scope entry could plausibly land on is
|
|
368
|
-
* predictable, auditable in a one-line diff, and does not touch legitimate
|
|
369
|
-
* deep grants under any other root.
|
|
370
|
-
* @type {ReadonlySet<string>}
|
|
371
|
-
*/
|
|
372
|
-
const DENIED_ABSOLUTE_TOP_SEGMENTS = new Set([
|
|
373
|
-
'etc',
|
|
374
|
-
'Users',
|
|
375
|
-
'home',
|
|
376
|
-
'root',
|
|
377
|
-
'bin',
|
|
378
|
-
'sbin',
|
|
379
|
-
'usr',
|
|
380
|
-
'System',
|
|
381
|
-
'var',
|
|
382
|
-
'boot',
|
|
383
|
-
'dev',
|
|
384
|
-
'proc',
|
|
385
|
-
'sys',
|
|
386
|
-
'Library',
|
|
387
|
-
'Applications',
|
|
388
|
-
'Windows',
|
|
389
|
-
]);
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* The top-level path segment of an absolute POSIX-style entry (the segment
|
|
393
|
-
* immediately after the leading "/"), if it is on the denylist above.
|
|
394
|
-
* @param {string} entry
|
|
395
|
-
* @returns {string|null}
|
|
396
|
-
*/
|
|
397
|
-
function deniedTopSegment(entry) {
|
|
398
|
-
const first = entry.split('/').filter(Boolean)[0];
|
|
399
|
-
return first && DENIED_ABSOLUTE_TOP_SEGMENTS.has(first) ? first : null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Does this entry contain a glob wildcard? Mirrors this codebase's own glob
|
|
404
|
-
* convention (`isGlobScopeEntry` in scripts/lib/scope-gate.mjs / #796): `*`
|
|
405
|
-
* is the sole wildcard metachar used in allowedPaths/fileScope entries
|
|
406
|
-
* throughout this repo (no `?`/`[]`/`{}` glob syntax is supported or tested
|
|
407
|
-
* anywhere else in scope-gate.mjs or enforce-scope.mjs).
|
|
408
|
-
* @param {string} entry
|
|
409
|
-
* @returns {boolean}
|
|
410
|
-
*/
|
|
411
|
-
function hasWildcard(entry) {
|
|
412
|
-
return entry.includes('*');
|
|
413
|
-
}
|
|
414
351
|
|
|
415
352
|
/**
|
|
416
353
|
* Validate allowedPaths array: must exist, be an array of non-empty strings,
|
|
@@ -463,37 +400,27 @@ function validateAllowedPaths(obj, errors, warnings) {
|
|
|
463
400
|
errors.push('allowedPaths contains empty string');
|
|
464
401
|
continue;
|
|
465
402
|
}
|
|
466
|
-
// #870
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
} else if (!hasWildcard(entry)) {
|
|
488
|
-
errors.push(
|
|
489
|
-
`allowedPaths contains a bare absolute file grant with no wildcard: ${entry} — a single concrete out-of-repo file has no established legitimate use in this codebase; scope a glob instead`,
|
|
490
|
-
);
|
|
491
|
-
} else {
|
|
492
|
-
warnings.push(
|
|
493
|
-
`allowedPaths contains an absolute (out-of-repo) path: ${entry} — honoured by hooks/enforce-scope.mjs Gate 5b; verify this grant is intentional`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
403
|
+
// #870 / #1398 / #1405 / #1406 — grade the entry as a Gate 5b grant through
|
|
404
|
+
// the SHARED predicate (`scripts/lib/scope-gate.mjs` gradeScopeEntry), the
|
|
405
|
+
// same one `hooks/enforce-scope.mjs` Gate 5b calls. Most absolute entries are
|
|
406
|
+
// a SANCTIONED out-of-repo grant and only WARN — the validator must not
|
|
407
|
+
// contradict the hook it validates for (#792/#870) — while the catastrophic
|
|
408
|
+
// subclasses (filesystem root, denylisted system directory in either its
|
|
409
|
+
// literal or its canonical spelling, a home grant at or above the user level
|
|
410
|
+
// or into a credential directory, a tilde that nothing expands, and a
|
|
411
|
+
// non-canonical prefix Gate 5b can never match) hard-reject here, BEFORE the
|
|
412
|
+
// hook is ever consulted. `null` = an ordinary relative entry, nothing to say.
|
|
413
|
+
//
|
|
414
|
+
// SAME PREDICATE, SAME RESOLVER (#1398 cond. 4). The resolver used to live
|
|
415
|
+
// here, which made the two callers' ARGUMENT shapes differ and let one grant
|
|
416
|
+
// reach two verdicts (`/tmp/x/**`: hook `warn`, this CLI `error`). Since the
|
|
417
|
+
// hot-path cost was measured (+0.067 ms per Gate 5b hit — see
|
|
418
|
+
// {@link canonicalizeGrantPrefix}) both callers pass this exact function, so
|
|
419
|
+
// the only remaining asymmetry is the CONSEQUENCE: exit-1 refusal here,
|
|
420
|
+
// allow-with-notice there.
|
|
421
|
+
const grade = gradeScopeEntry(entry, { resolve: canonicalizeGrantPrefix });
|
|
422
|
+
if (grade !== null) {
|
|
423
|
+
(grade.verdict === 'error' ? errors : warnings).push(`allowedPaths ${grade.message}`);
|
|
497
424
|
}
|
|
498
425
|
// Reject path traversal: any `../` segment. INDEPENDENT of the absolute checks
|
|
499
426
|
// above — an absolute entry that ALSO contains `../` must still be rejected
|
package/scripts/vault-mirror.mjs
CHANGED
|
@@ -485,6 +485,10 @@ async function main() {
|
|
|
485
485
|
kind,
|
|
486
486
|
force,
|
|
487
487
|
vaultName,
|
|
488
|
+
// #1389: the repo whose `.vault.yaml` declares the namespace slug. This CLI
|
|
489
|
+
// has no repo-root flag, so it is the invocation cwd — bound ONCE here and
|
|
490
|
+
// shared by every processor call and the auto-commit below.
|
|
491
|
+
repoRoot: process.cwd(),
|
|
488
492
|
qualityMinNarrativeChars,
|
|
489
493
|
qualityMinConfidence,
|
|
490
494
|
};
|
|
@@ -693,7 +697,11 @@ async function main() {
|
|
|
693
697
|
// Opt-in: triggers only when --session-id is provided. Callers (session-end, evolve)
|
|
694
698
|
// pass it explicitly; bare invocations stay quiet to preserve legacy behaviour.
|
|
695
699
|
if (!dryRun && !noCommit && sessionIdArg) {
|
|
696
|
-
autoCommitVaultMirror(
|
|
700
|
+
autoCommitVaultMirror(
|
|
701
|
+
resolve(vaultDir),
|
|
702
|
+
sessionIdArg,
|
|
703
|
+
resolveRepoNamespace({ vaultName, repoRoot: ctx.repoRoot }),
|
|
704
|
+
);
|
|
697
705
|
}
|
|
698
706
|
}
|
|
699
707
|
|
|
@@ -8,15 +8,17 @@ The current platform is determined by the `scripts/lib/platform.mjs` library:
|
|
|
8
8
|
- `$SO_PLATFORM` = `claude` | `codex` | `cursor` | `pi`
|
|
9
9
|
- Environment: `$CLAUDE_PLUGIN_ROOT` (Claude Code), `$CODEX_PLUGIN_ROOT` (Codex CLI), `$CURSOR_RULES_DIR` (Cursor IDE), or `$PI_PLUGIN_ROOT` (Pi)
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
11
|
+
## File and Shell Tool Adaptation
|
|
12
|
+
|
|
13
|
+
Skill bodies use Claude Code names as operations, not as a requirement that every
|
|
14
|
+
harness expose those exact tools. Use the current runtime's available tools.
|
|
15
|
+
Measured in Codex Desktop 0.153.4 on 2026-09-19: `exec_command` runs shell
|
|
16
|
+
commands (`cat`, `rg`, `rg --files` for Read/Grep/Glob); `apply_patch` performs
|
|
17
|
+
file edits. These may be exposed through a `functions.exec` tools namespace.
|
|
18
|
+
Never call an unavailable `Read`, `Bash`, or `Skill` tool by name. An instruction
|
|
19
|
+
to invoke `session-orchestrator:<name>` means read and follow the canonical
|
|
20
|
+
`skills/<name>/SKILL.md` relative to the plugin root when no Skill tool exists.
|
|
21
|
+
Keep the user's project as the working directory for project operations.
|
|
20
22
|
|
|
21
23
|
## Platform-Specific Tool Mapping
|
|
22
24
|
|
|
@@ -24,7 +26,7 @@ These tools have the same name and behavior on all platforms. Cursor IDE uses eq
|
|
|
24
26
|
|
|
25
27
|
| Function | Claude Code | Codex CLI | Cursor IDE | Pi |
|
|
26
28
|
|----------|------------|-----------|------------|----|
|
|
27
|
-
| Present choices to user | `AskUserQuestion` tool with structured options |
|
|
29
|
+
| Present choices to user | `AskUserQuestion` tool with structured options | Available native user-input tool, respecting its mode restrictions; plain-text fallback when unavailable | Available native choice tool or numbered Markdown list | Numbered Markdown list v1; native UI adapter planned |
|
|
28
30
|
| Dispatch subagent | `Agent({ description, prompt, subagent_type })` | Native multi-agent collaboration namespace: `spawn_agent`, `list_agents`, `wait_agent`, `send_message`, `followup_task`, `interrupt_agent`, `close_agent`. Measured 2026-08-25 on `codex-cli 0.141.0` — `codex features list` → `multi_agent stable true`. | No native in-session Agent tool. Coordinator-side foreign dispatch via the headless `cursor-agent` CLI exists (`scripts/lib/wave-executor/foreign-dispatch.mjs`, #1150) — measured 2026-08-25 on `cursor-agent 2026.08.11-e8db854`. | Sequential execution v1. Do not assume native subagents until the Pi SDK dispatcher exists. |
|
|
29
31
|
| Track tasks | `TaskCreate` / `TaskUpdate` / `TaskList` | Plain-text checklist in response context | Plain-text checklist (same as Codex) | Plain-text checklist (same as Codex) |
|
|
30
32
|
| Enter plan mode | `EnterPlanMode` / `ExitPlanMode` tools | `/plan` slash command (prompt-level, not tool-based) | Instruction-based: "Focus on analysis and planning. Do not modify files until the user approves." | `/plan` prompt template; use instruction-based planning when tool mode is unavailable |
|
|
@@ -37,7 +39,13 @@ When a skill instructs "Use the AskUserQuestion tool", apply this pattern:
|
|
|
37
39
|
|
|
38
40
|
**On Claude Code:** Use the AskUserQuestion tool with structured options as documented.
|
|
39
41
|
|
|
40
|
-
**On Codex
|
|
42
|
+
**On Codex:** Prefer the available native user-input tool. In the measured Desktop
|
|
43
|
+
0.153.4 runtime (2026-09-19), `request_user_input_async` allows a question while
|
|
44
|
+
independent work continues; `request_user_input` has mode restrictions. Follow
|
|
45
|
+
the live tool contract. Existing user authorization remains valid; do not ask
|
|
46
|
+
again merely because a workflow names `AskUserQuestion`.
|
|
47
|
+
|
|
48
|
+
**When no suitable native tool is available (including Cursor IDE / Pi):** Present the same choices as a numbered Markdown list and ask the user to respond:
|
|
41
49
|
```
|
|
42
50
|
Choose one:
|
|
43
51
|
1. Option A — description
|
|
@@ -85,6 +93,10 @@ No Session Orchestrator Pi subagent dispatcher exists in v1. Execute wave tasks
|
|
|
85
93
|
|
|
86
94
|
Skills use `model-preference` (Claude), `model-preference-codex` (Codex), `model-preference-cursor` (Cursor), and active-model fallback on Pi in YAML frontmatter.
|
|
87
95
|
|
|
96
|
+
These are preferences, not authority to replace a user's selected model,
|
|
97
|
+
reasoning effort, or service tier. Preserve the current settings unless the user
|
|
98
|
+
requests a change; inherit them for native subagents.
|
|
99
|
+
|
|
88
100
|
## State Directory
|
|
89
101
|
|
|
90
102
|
- **Claude Code:** `.claude/` (STATE.md, wave-scope.json)
|