session-orchestrator 3.20.0 → 3.22.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/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +7 -5
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secret-masker.mjs — value-based secret masking over arbitrary log text (#974).
|
|
3
|
+
*
|
|
4
|
+
* Builds a masking function from an env-like object: every value that survives
|
|
5
|
+
* three filters becomes a NEEDLE, and every occurrence of a needle in the input
|
|
6
|
+
* text is replaced with the repo-wide `[REDACTED]` marker.
|
|
7
|
+
*
|
|
8
|
+
* Pure and synchronous — no I/O, no module state, no dependencies beyond the
|
|
9
|
+
* sibling `redact-spans.mjs` primitive — because hot-path consumers import it.
|
|
10
|
+
* This module deliberately does NOT wire itself into any consumer; wiring is a
|
|
11
|
+
* separate concern (and a separate wave).
|
|
12
|
+
*
|
|
13
|
+
* ---------------------------------------------------------------------------
|
|
14
|
+
* THE NEEDLE SET IS A FUNCTION OF THE CALLER'S ENV — AND THAT IS NOT A DEFECT
|
|
15
|
+
* ---------------------------------------------------------------------------
|
|
16
|
+
* Two runs of the same consumer over the same records mask DIFFERENTLY when the
|
|
17
|
+
* env differs between them (#1025): with `FOO_TOKEN` set, its value becomes
|
|
18
|
+
* `[REDACTED]`; without it, the same text passes through verbatim. Consumers that
|
|
19
|
+
* compare a previously-written artifact against a freshly-rendered candidate must
|
|
20
|
+
* therefore treat an already-redacted span as a WILDCARD, or a later
|
|
21
|
+
* partially-populated run re-writes the raw value it had already redacted (see
|
|
22
|
+
* `matchesModuloRedaction` in `scripts/lib/vault-mirror/process.mjs`).
|
|
23
|
+
*
|
|
24
|
+
* The tempting fix — persist the needle set so a later run can mask without the
|
|
25
|
+
* env — is REJECTED: it breaks the purity contract in the paragraph above and
|
|
26
|
+
* puts a plaintext secrets file on disk to defend against secrets on disk. The
|
|
27
|
+
* env dependency stays; consumers compensate.
|
|
28
|
+
*
|
|
29
|
+
* ---------------------------------------------------------------------------
|
|
30
|
+
* WHY THE FILTER ORDER IS ALLOWLIST → KEY-NAME → LENGTH, AND NOT LENGTH FIRST
|
|
31
|
+
* ---------------------------------------------------------------------------
|
|
32
|
+
* A "mask every env value longer than N characters" masker is the obvious
|
|
33
|
+
* design and it is measurably WRONG. Discovery measured 40 env values of ≥8
|
|
34
|
+
* characters on the development host against 5.2 MB of real log text; six of
|
|
35
|
+
* them collide with ordinary log content:
|
|
36
|
+
*
|
|
37
|
+
* len 21 → 157 corpus hits (USER, LOGNAME)
|
|
38
|
+
* len 28 → 157 corpus hits (HOME)
|
|
39
|
+
* len 58 → 154 corpus hits (PWD, OLDPWD)
|
|
40
|
+
* len 36 → 39 corpus hits (CLAUDE_CODE_SESSION_ID)
|
|
41
|
+
*
|
|
42
|
+
* The shortest REAL secret on that host is 32 characters; the longest collider
|
|
43
|
+
* is 58. The bands overlap completely: any threshold ≤58 admits PWD (154
|
|
44
|
+
* mis-maskings), any threshold >58 discards three real secrets (32/50/51). There
|
|
45
|
+
* is no separating value — length is provably not a discriminating feature.
|
|
46
|
+
* Length therefore runs LAST, purely as a guard against degenerate short values
|
|
47
|
+
* (`X_KEY=1`), never as the primary filter.
|
|
48
|
+
*
|
|
49
|
+
* ---------------------------------------------------------------------------
|
|
50
|
+
* CLAUDE_CODE_SESSION_ID — the sharpest boundary case, and why it is NOT
|
|
51
|
+
* allowlisted
|
|
52
|
+
* ---------------------------------------------------------------------------
|
|
53
|
+
* It is 36 characters with 39 corpus hits: length-indistinguishable from a
|
|
54
|
+
* 32-character token, yet a legitimate correlation key that appears in ordinary
|
|
55
|
+
* log lines. Masking it would destroy the log's correlation value; not masking
|
|
56
|
+
* it costs nothing, because a session id is not a credential (the credential
|
|
57
|
+
* beside it is CLAUDE_CODE_MESSAGING_TOKEN, which DOES qualify via `_TOKEN`).
|
|
58
|
+
*
|
|
59
|
+
* The decision is to leave it OUT of the allowlist and let filter 2 exclude it
|
|
60
|
+
* structurally: it ends in `_ID`, which the key-name heuristic does not match,
|
|
61
|
+
* so it can never become a needle. An allowlist entry would be redundant today
|
|
62
|
+
* AND silent tomorrow — if someone later widens the heuristic to `_ID`, an
|
|
63
|
+
* allowlist entry would quietly absorb the change, whereas the pinned test
|
|
64
|
+
* (`tests/lib/secret-masker.test.mjs`) goes RED and forces the decision to be
|
|
65
|
+
* taken again. The louder mechanism wins.
|
|
66
|
+
*
|
|
67
|
+
* ---------------------------------------------------------------------------
|
|
68
|
+
* NEEDLE SET IS `{raw}` ONLY — no base64, no percent-encoding
|
|
69
|
+
* ---------------------------------------------------------------------------
|
|
70
|
+
* Encoded variants are theoretical in this repo: the only encoders encode FILE
|
|
71
|
+
* PATHS (`scripts/lib/fetch-baseline.mjs`, `scripts/lib/vault-backfill/glab.mjs`)
|
|
72
|
+
* or HOOK SOURCE (`hooks/_lib/guard-source-loader.mjs`) — never credentials. And
|
|
73
|
+
* typical token alphabets are URI-unreserved, so percent-encoding would be the
|
|
74
|
+
* identity transform for them anyway. Each extra variant triples the needle set
|
|
75
|
+
* and with it the collision surface, buying coverage for no reachable path.
|
|
76
|
+
*
|
|
77
|
+
* @see scripts/lib/redact-spans.mjs — the overlap-safe span merge this delegates to
|
|
78
|
+
* @see scripts/lib/quality-gate/diagnostics.mjs — SECRET_ENV_NAME_RE, the shape mirrored below
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
import { redactSpans } from './redact-spans.mjs';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Filter 1 — deny-by-default for MASKING: these keys never become needles.
|
|
85
|
+
*
|
|
86
|
+
* Measured, not guessed: every entry either appears verbatim in ordinary log
|
|
87
|
+
* text (paths, usernames, terminal identity) or is a public toolchain constant.
|
|
88
|
+
* Masking any of them corrupts the log without protecting anything.
|
|
89
|
+
*
|
|
90
|
+
* HONEST NOTE ON ITS CURRENT REACH — measured, not assumed: no key in this set
|
|
91
|
+
* ends in a secret-bearing suffix, so filter 2 alone already rejects every one
|
|
92
|
+
* of them. Proven by a fake-regression probe: with this whole set bypassed and
|
|
93
|
+
* filter 2 intact, the over-masking tests in `tests/lib/secret-masker.test.mjs`
|
|
94
|
+
* stayed GREEN (16/17 — the single failure was the unrelated escaping probe run
|
|
95
|
+
* in the same pass). This set is therefore defense-in-depth plus a record of the
|
|
96
|
+
* measurement — NOT the load-bearing filter. Do not read a green over-masking
|
|
97
|
+
* test as proof that this list stopped the collision; the key-name heuristic
|
|
98
|
+
* did. Adding a secret-NAMED key here would open a silent hole in which a real
|
|
99
|
+
* credential is never masked, so any such addition needs its own justification
|
|
100
|
+
* beside the entry.
|
|
101
|
+
*
|
|
102
|
+
* @type {ReadonlySet<string>}
|
|
103
|
+
*/
|
|
104
|
+
const NEVER_MASK_KEYS = new Set([
|
|
105
|
+
// A — POSIX / shell identity and paths; guaranteed to occur in log text.
|
|
106
|
+
'PATH',
|
|
107
|
+
'FPATH',
|
|
108
|
+
'MANPATH',
|
|
109
|
+
'INFOPATH',
|
|
110
|
+
'XDG_DATA_DIRS',
|
|
111
|
+
'HOME',
|
|
112
|
+
'PWD',
|
|
113
|
+
'OLDPWD',
|
|
114
|
+
'TMPDIR',
|
|
115
|
+
'SHELL',
|
|
116
|
+
'USER',
|
|
117
|
+
'LOGNAME',
|
|
118
|
+
'LANG',
|
|
119
|
+
'TERM',
|
|
120
|
+
'TERMINFO',
|
|
121
|
+
'COLORTERM',
|
|
122
|
+
'COMMAND_MODE',
|
|
123
|
+
'_',
|
|
124
|
+
// B — toolchain install prefixes; public constants that appear in every
|
|
125
|
+
// build/tool log line.
|
|
126
|
+
'HOMEBREW_PREFIX',
|
|
127
|
+
'HOMEBREW_REPOSITORY',
|
|
128
|
+
'HOMEBREW_CELLAR',
|
|
129
|
+
'BUN_INSTALL',
|
|
130
|
+
'PNPM_HOME',
|
|
131
|
+
'DOTNET_ROOT',
|
|
132
|
+
// C — terminal / platform identity.
|
|
133
|
+
'GHOSTTY_SHELL_FEATURES',
|
|
134
|
+
'GHOSTTY_BIN_DIR',
|
|
135
|
+
'GHOSTTY_RESOURCES_DIR',
|
|
136
|
+
'__CF_USER_TEXT_ENCODING',
|
|
137
|
+
'__CFBundleIdentifier',
|
|
138
|
+
'AI_AGENT',
|
|
139
|
+
'CLAUDE_CODE_EXECPATH',
|
|
140
|
+
'SSH_AUTH_SOCK',
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Filter 2 — key-name heuristic: only semantically secret-bearing NAMES qualify.
|
|
145
|
+
*
|
|
146
|
+
* This MIRRORS the shape of `SECRET_ENV_NAME_RE` in
|
|
147
|
+
* `scripts/lib/quality-gate/diagnostics.mjs:49` but is deliberately a SEPARATE
|
|
148
|
+
* declaration rather than an import. Three reasons:
|
|
149
|
+
*
|
|
150
|
+
* 1. Layering. That module is a quality-gate diagnostics helper; this one must
|
|
151
|
+
* be importable from a hot path. Importing it would drag the diagnostics
|
|
152
|
+
* layer (and its redaction-pattern table) into every consumer for one regex.
|
|
153
|
+
* 2. Different populations. There the input is a captured env SNAPSHOT inside a
|
|
154
|
+
* JSON bundle and the effect is "replace the value in place"; here the input
|
|
155
|
+
* is the live `process.env` of the host and the effect is "search for this
|
|
156
|
+
* value in unrelated text". The second is far more destructive when it
|
|
157
|
+
* over-matches, so the two are free to diverge — and SHOULD be.
|
|
158
|
+
* 3. Recorded prior art: consolidating a primitive does not consolidate what
|
|
159
|
+
* its call sites feed it (learnings-index, conf 0.9). A shared regex here
|
|
160
|
+
* would create exactly that illusion of a single decision point.
|
|
161
|
+
*
|
|
162
|
+
* Two deliberate widenings over the mirrored shape:
|
|
163
|
+
* - a bare name (`TOKEN`, `PASSWORD`, `SECRET`, `API_KEY`-less `KEY`) qualifies;
|
|
164
|
+
* the diagnostics form requires at least one character before the suffix.
|
|
165
|
+
* - the key is upper-cased before testing, so lowercase env keys
|
|
166
|
+
* (`github_token`) qualify too. `npm_config_*` keys are unaffected — none of
|
|
167
|
+
* them end in a secret-bearing suffix.
|
|
168
|
+
*/
|
|
169
|
+
const SECRET_KEY_RE =
|
|
170
|
+
/^(?:[A-Z][A-Z0-9_]*(?:_TOKEN|_KEY|_SECRET|_PASSWORD|_CREDENTIAL[A-Z_]*)|TOKEN|KEY|SECRET|PASSWORD)$/;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Filter 3 — minimum value length.
|
|
174
|
+
*
|
|
175
|
+
* NAMED CEILING (this is a deliberate simplification, per the repo's
|
|
176
|
+
* build-value rule): 8 is a DEGENERATE-VALUE guard, not a discriminator. It
|
|
177
|
+
* exists so that a secret-named key carrying a trivial value (`X_KEY=1`,
|
|
178
|
+
* `MY_TOKEN=abc`) cannot turn into a needle that shreds every line of the log.
|
|
179
|
+
* It is explicitly NOT tuned to separate secrets from non-secrets — the header
|
|
180
|
+
* shows that no such threshold exists on the measured host.
|
|
181
|
+
*
|
|
182
|
+
* REVISIT TRIGGER: if a consumer reports over-masking, the answer is an
|
|
183
|
+
* allowlist entry or a narrower key-name heuristic — NOT a higher threshold.
|
|
184
|
+
* Raising it past 32 starts discarding real secrets; every value below 58 still
|
|
185
|
+
* admits PWD. Any patch that moves this number is treating a measurement as a
|
|
186
|
+
* dial.
|
|
187
|
+
*/
|
|
188
|
+
const MIN_MASKABLE_LENGTH = 8;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Decide whether a single env entry qualifies as a masking needle.
|
|
192
|
+
*
|
|
193
|
+
* The three filters run in the order documented in the module header:
|
|
194
|
+
* allowlist → key-name heuristic → length.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} key
|
|
197
|
+
* @param {unknown} value
|
|
198
|
+
* @returns {boolean}
|
|
199
|
+
*/
|
|
200
|
+
function qualifiesAsNeedle(key, value) {
|
|
201
|
+
if (typeof key !== 'string' || typeof value !== 'string') return false;
|
|
202
|
+
if (NEVER_MASK_KEYS.has(key)) return false; // 1 — allowlist
|
|
203
|
+
if (!SECRET_KEY_RE.test(key.toUpperCase())) return false; // 2 — key-name heuristic
|
|
204
|
+
if (value.length < MIN_MASKABLE_LENGTH) return false; // 3 — degenerate-value guard
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build a value-based secret masker from an env-like object.
|
|
210
|
+
*
|
|
211
|
+
* Matching is LITERAL and CASE-SENSITIVE, with no word boundaries — two
|
|
212
|
+
* deliberate departures from the confidential-names patterns that
|
|
213
|
+
* `redactSpans` was extracted for:
|
|
214
|
+
* - No `\b`: a secret is not a word. Tokens routinely abut quotes, `=`, `:`
|
|
215
|
+
* and newlines, and many end in `-` or `_`, where `\b` simply fails to
|
|
216
|
+
* anchor. A boundary condition here would silently skip real hits.
|
|
217
|
+
* - Case-sensitive: secrets are case-sensitive by construction, and folding
|
|
218
|
+
* case only widens the collision surface for free.
|
|
219
|
+
* Every value is regex-escaped before it becomes a pattern, so a secret
|
|
220
|
+
* containing `.`, `(`, `[` or `*` matches itself and nothing else.
|
|
221
|
+
* (`RegExp.escape` is stdlib from Node 23.5; this package requires `>=24.0.0`.)
|
|
222
|
+
*
|
|
223
|
+
* Returns an OBJECT rather than a bare function so the needle count is
|
|
224
|
+
* available to callers without a function-property trick: a consumer can log
|
|
225
|
+
* "masking active, N needles" (a count is safe to print; a value never is) and
|
|
226
|
+
* can branch on `needleCount === 0` for a fast path. The masking function
|
|
227
|
+
* itself is `mask`.
|
|
228
|
+
*
|
|
229
|
+
* @param {Record<string, unknown>} env — e.g. `process.env`. A non-object
|
|
230
|
+
* yields a masker with zero needles (identity) rather than throwing: a hot
|
|
231
|
+
* path must not die on a malformed argument, and there is genuinely nothing
|
|
232
|
+
* to mask in that case.
|
|
233
|
+
* @returns {{ mask: (text: string) => string, needleCount: number }}
|
|
234
|
+
*/
|
|
235
|
+
export function createSecretValueMasker(env) {
|
|
236
|
+
/** @type {Set<string>} deduped — two keys sharing a value need one pattern. */
|
|
237
|
+
const values = new Set();
|
|
238
|
+
if (env && typeof env === 'object') {
|
|
239
|
+
for (const [key, value] of Object.entries(env)) {
|
|
240
|
+
if (qualifiesAsNeedle(key, value)) values.add(/** @type {string} */ (value));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const patterns = [...values].map((v) => new RegExp(RegExp.escape(v)));
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Replace every needle occurrence in `text` with `[REDACTED]`.
|
|
248
|
+
*
|
|
249
|
+
* Delegates the overlap-safe interval merge to `redactSpans`, so a needle
|
|
250
|
+
* that is a prefix of another needle yields ONE marker, not a nested pair —
|
|
251
|
+
* no splice logic is re-derived here.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} text
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
const mask = (text) => {
|
|
257
|
+
if (patterns.length === 0 || typeof text !== 'string') return text;
|
|
258
|
+
return redactSpans(text, patterns);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return { mask, needleCount: patterns.length };
|
|
262
|
+
}
|
|
@@ -161,6 +161,14 @@ function lockAgeHours(lock) {
|
|
|
161
161
|
* effective heartbeat, so TTL freshness still rescues recent locks even when
|
|
162
162
|
* the writer process is dead (the D2/D5 production case).
|
|
163
163
|
*
|
|
164
|
+
* RETAINED, not forgotten (#595, re-verified 2026-08-15): zero v1 files exist
|
|
165
|
+
* on this host, but this normalisation is MIRRORED in
|
|
166
|
+
* `scripts/lib/harness-audit/categories/category4.mjs` `lockIsLive()` and
|
|
167
|
+
* pinned by `tests/lib/lock-ttl-parity.test.mjs`. Removing it here alone
|
|
168
|
+
* breaks that parity by construction — see
|
|
169
|
+
* `skills/_shared/state-ownership.md` § Schema v1 Sunset for the full
|
|
170
|
+
* co-change set the removal needs.
|
|
171
|
+
*
|
|
164
172
|
* @param {string} raw
|
|
165
173
|
* @returns {object|null}
|
|
166
174
|
*/
|
|
@@ -230,6 +238,12 @@ function buildLock({ sessionId, mode, ttlHours, semanticSessionId }) {
|
|
|
230
238
|
*
|
|
231
239
|
* Liveness rule: a lock is live when (now - last_heartbeat) < ttl_hours.
|
|
232
240
|
*
|
|
241
|
+
* The `started_at` fallback below is RETAINED, not forgotten (#595,
|
|
242
|
+
* re-verified 2026-08-15): it is mirrored verbatim in
|
|
243
|
+
* `scripts/lib/harness-audit/categories/category4.mjs` `lockIsLive()` and
|
|
244
|
+
* pinned by `tests/lib/lock-ttl-parity.test.mjs`. See
|
|
245
|
+
* `skills/_shared/state-ownership.md` § Schema v1 Sunset.
|
|
246
|
+
*
|
|
233
247
|
* @param {{ last_heartbeat: string, started_at: string, ttl_hours?: number }} lock
|
|
234
248
|
* @param {number} [nowMs]
|
|
235
249
|
* @returns {boolean}
|
|
@@ -863,13 +877,18 @@ export function loadOwnerProof({ repoRoot } = {}) {
|
|
|
863
877
|
* (see `buildLockOwnerProof()` / `isLockOwnedByProof()`) to require a SECOND
|
|
864
878
|
* identity factor beyond `session_id` before deleting — this is what makes
|
|
865
879
|
* a same-day semantic-id collision safe to release against. When `proof` is
|
|
866
|
-
*
|
|
867
|
-
* only the `session_id` match gates the delete.
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
880
|
+
* ABSENT (omitted, `undefined`, or `null`), behaviour is BYTE-IDENTICAL to
|
|
881
|
+
* before this change: only the `session_id` match gates the delete.
|
|
882
|
+
*
|
|
883
|
+
* `null` is treated as absent DELIBERATELY (#989): the two producers of a
|
|
884
|
+
* proof — `loadOwnerProof()` and `buildLockOwnerProof()` — both return `null`
|
|
885
|
+
* to mean "cannot prove ownership". Gating on `proof !== undefined` forced
|
|
886
|
+
* every call site to spread-guard (`...(proof ? { proof } : {})`) or brick
|
|
887
|
+
* release with a permanent `proof-mismatch`; treating both absent spellings
|
|
888
|
+
* alike moves that contract inside the API where it belongs. A caller that
|
|
889
|
+
* wants fail-closed-on-unprovable must branch itself before calling.
|
|
871
890
|
*
|
|
872
|
-
* @param {{ sessionId: string, repoRoot?: string, proof?: { pid: number, host: string, startedAt: string } }} args
|
|
891
|
+
* @param {{ sessionId: string, repoRoot?: string, proof?: { pid: number, host: string, startedAt: string }|null }} args
|
|
873
892
|
* @returns {{ ok: true, deleted: boolean, reason?: string, verified?: boolean }}
|
|
874
893
|
*/
|
|
875
894
|
export function release({ sessionId, repoRoot, proof } = {}) {
|
|
@@ -886,11 +905,16 @@ export function release({ sessionId, repoRoot, proof } = {}) {
|
|
|
886
905
|
}
|
|
887
906
|
|
|
888
907
|
// Proof-gated release (additive, #906-class fix): when the caller supplies
|
|
889
|
-
//
|
|
908
|
+
// a proof, the session_id match above is NOT sufficient by itself —
|
|
890
909
|
// session_id collisions are the documented root cause behind this check.
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
|
|
910
|
+
// Absent-proof handling (#989): an absent proof — omitted, `undefined`, or
|
|
911
|
+
// the `null` that loadOwnerProof()/buildLockOwnerProof() return for
|
|
912
|
+
// "cannot prove" — leaves this branch dead code and preserves the exact
|
|
913
|
+
// pre-existing session_id-only behaviour. No call-site spread-guard needed.
|
|
914
|
+
// Spelled out rather than as `proof != null` because the repo's `eqeqeq`
|
|
915
|
+
// lint rule admits no null-loose exception.
|
|
916
|
+
const proofSupplied = proof !== undefined && proof !== null;
|
|
917
|
+
if (proofSupplied && !isLockOwnedByProof(existing, proof)) {
|
|
894
918
|
return { ok: true, deleted: false, reason: 'proof-mismatch' };
|
|
895
919
|
}
|
|
896
920
|
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* registry-sourced peer was bucketed as `mode='session'` → classifyMode threw
|
|
24
24
|
* → fell back to `parallel-ok`, silently bypassing the exclusivity matrix
|
|
25
25
|
* for cross-repo entries (D5 from Epic #583 audit). The field is optional
|
|
26
|
-
* on read for back-compat with v1 entries (defaults to null)
|
|
26
|
+
* on read for back-compat with v1 entries (defaults to null) — retained
|
|
27
|
+
* deliberately, see `skills/_shared/state-ownership.md` § Schema v1 Sunset.
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
30
|
import os from 'node:os';
|
|
@@ -115,6 +116,13 @@ function _validEntry(obj) {
|
|
|
115
116
|
// Schema v2 (Epic #583): `mode` is optional. When present it MUST be a string
|
|
116
117
|
// (no number / object / array smuggling). When absent (v1 entry), it is
|
|
117
118
|
// accepted — back-compat with pre-#583 registry files.
|
|
119
|
+
//
|
|
120
|
+
// RETAINED, not forgotten (#595, re-verified 2026-08-15): zero mode-less
|
|
121
|
+
// entries exist on this host, but rejecting one would make a LIVE peer
|
|
122
|
+
// invisible to the exclusivity matrix — a strict weakening of
|
|
123
|
+
// parallel-session detection for zero functional gain (an absent mode
|
|
124
|
+
// already degrades to the `parallel-ok` bucket). See
|
|
125
|
+
// `skills/_shared/state-ownership.md` § Schema v1 Sunset.
|
|
118
126
|
if ('mode' in obj && obj.mode !== null && typeof obj.mode !== 'string') {
|
|
119
127
|
return false;
|
|
120
128
|
}
|
|
@@ -35,8 +35,14 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { execFileSync } from 'node:child_process';
|
|
38
|
+
import { readFileSync } from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
38
40
|
import { digestSha256Short } from './crypto-digest-utils.mjs';
|
|
39
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
chargeIssueBudget,
|
|
43
|
+
formatBlockReason,
|
|
44
|
+
resolveIssueBudgetSessionId,
|
|
45
|
+
} from './issue-budget.mjs';
|
|
40
46
|
import { resolveRepoSpec } from './vcs-repo-spec.mjs';
|
|
41
47
|
|
|
42
48
|
/**
|
|
@@ -99,10 +105,25 @@ function runCli(cmd, args) {
|
|
|
99
105
|
if (isIssueCreateArgv(cmd, args)) {
|
|
100
106
|
try {
|
|
101
107
|
const repoRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
108
|
+
// The harness exports CLAUDE_CODE_SESSION_ID (measured 2026-08-21: it is
|
|
109
|
+
// present in the Bash tool environment this module runs in). There is no
|
|
110
|
+
// CLAUDE_SESSION_ID — reading that name made this whole block dead code
|
|
111
|
+
// and left the cap permanently off on the programmatic path.
|
|
112
|
+
const nativeRawId = process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
113
|
+
let currentSession = null;
|
|
114
|
+
if (typeof nativeRawId === 'string' && nativeRawId.length > 0) {
|
|
115
|
+
try {
|
|
116
|
+
currentSession = JSON.parse(
|
|
117
|
+
readFileSync(path.join(repoRoot, '.orchestrator', 'current-session.json'), 'utf8'),
|
|
118
|
+
);
|
|
119
|
+
} catch {
|
|
120
|
+
// Missing or malformed records conservatively retain the native env key.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
102
123
|
const titleIdx = args.indexOf('--title');
|
|
103
124
|
const verdict = chargeIssueBudget({
|
|
104
125
|
repoRoot,
|
|
105
|
-
sessionId:
|
|
126
|
+
sessionId: resolveIssueBudgetSessionId(nativeRawId, currentSession),
|
|
106
127
|
command: [cmd, ...args].join(' '),
|
|
107
128
|
title: titleIdx >= 0 ? (args[titleIdx + 1] ?? null) : null,
|
|
108
129
|
});
|