session-orchestrator 3.16.0 → 3.19.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/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +130 -412
- package/README.md +17 -12
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/README.md +2 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +306 -6
- package/docs/session-config-template.md +66 -3
- package/docs/telemetry/telemetry-claims.md +204 -0
- package/docs/telemetry.md +158 -0
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/skill-invocation-telemetry.mjs +109 -10
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +16 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/compute-grounding-injection.sh +18 -3
- package/scripts/dialectic-deriver.mjs +7 -2
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/auto-dialectic.mjs +11 -2
- package/scripts/lib/auto-dream.mjs +16 -5
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/build-live-signals.mjs +7 -4
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/context-coverage.mjs +82 -0
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/moc-staleness.mjs +98 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config/worktree-orphans.mjs +138 -0
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +32 -0
- package/scripts/lib/context-coverage-banner.mjs +223 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +136 -30
- package/scripts/lib/dispatcher/rank.mjs +22 -8
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/moc-staleness-banner.mjs +267 -0
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/filters.mjs +88 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/skill-health/join.mjs +35 -9
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/telemetry/anon-id.mjs +141 -0
- package/scripts/lib/telemetry/consent.mjs +299 -0
- package/scripts/lib/telemetry/paths.mjs +27 -0
- package/scripts/lib/telemetry/queue.mjs +287 -0
- package/scripts/lib/telemetry/schema.mjs +384 -0
- package/scripts/lib/telemetry/sync.mjs +312 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vault-status/board-writer.mjs +63 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +32 -6
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/telemetry.mjs +250 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +97 -0
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +108 -14
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +149 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/SKILL.md +1 -1
- package/skills/vault-sync/package-lock.json +3 -3
- package/skills/vault-sync/validator.mjs +228 -40
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- package/templates/_shared/loop.md +4 -4
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* G7 transcript inspection via hooks/_lib/transcript-history.mjs.
|
|
23
23
|
* If any prior Read tool call matches one of the host-specific template
|
|
24
24
|
* paths from the policy, exit 0.
|
|
25
|
-
* G8 fall-through: emit
|
|
26
|
-
* template
|
|
25
|
+
* G8 fall-through: emit the PreToolUse deny envelope on stdout via emitDeny
|
|
26
|
+
* (exit 0) with the template-path list + ack hint inside the reason.
|
|
27
27
|
*
|
|
28
28
|
* Fail-safe posture: any internal exception is swallowed in main().catch and
|
|
29
29
|
* the hook exits 0 (allow). Rationale matches pre-bash-destructive-guard.mjs:
|
|
@@ -31,14 +31,17 @@
|
|
|
31
31
|
* worst case is a missed enforcement, not a wedged session.
|
|
32
32
|
*
|
|
33
33
|
* Exit codes:
|
|
34
|
-
* 0 —
|
|
35
|
-
*
|
|
34
|
+
* 0 — every path. Pass-through (G1-G3 short-circuits, bypass match,
|
|
35
|
+
* acknowledgement, Read found, error) emits nothing; G8 emits the deny
|
|
36
|
+
* envelope on stdout. Exit 2 is NEVER used: Claude Code discards stdout
|
|
37
|
+
* on exit 2, which would throw away the deny envelope entirely (#906).
|
|
36
38
|
*/
|
|
37
39
|
|
|
38
|
-
import { readStdin, emitAllow } from '../scripts/lib/io.mjs';
|
|
40
|
+
import { readStdin, emitAllow, emitDeny } from '../scripts/lib/io.mjs';
|
|
39
41
|
import { resolveProjectDir, resolvePluginRoot } from '../scripts/lib/platform.mjs';
|
|
40
42
|
import { readJson } from '../scripts/lib/common.mjs';
|
|
41
43
|
import { hasReadInSession } from './_lib/transcript-history.mjs';
|
|
44
|
+
import { resolveHost, matchesBypass } from './_lib/vcs-create-matcher.mjs';
|
|
42
45
|
|
|
43
46
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
44
47
|
import { existsSync, readdirSync, lstatSync } from 'node:fs';
|
|
@@ -54,13 +57,11 @@ if (!shouldRunHook('pre-bash-templates-first')) process.exit(0);
|
|
|
54
57
|
// ---------------------------------------------------------------------------
|
|
55
58
|
|
|
56
59
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* are deliberately out of scope per PRD § 2 Out-of-Scope.
|
|
60
|
+
* The `gh`/`glab` create matcher (`CREATE_REGEX`, `resolveHost`,
|
|
61
|
+
* `matchesBypass`) now lives in `hooks/_lib/vcs-create-matcher.mjs` so the
|
|
62
|
+
* sibling issue-budget hook parses create commands with byte-identical
|
|
63
|
+
* semantics instead of a diverging copy. Behaviour here is unchanged.
|
|
62
64
|
*/
|
|
63
|
-
const CREATE_REGEX = /^\s*(gh|glab)\s+(pr|mr|issue)\s+(create|new)\b/;
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Default acknowledgement path, relative to project root. Used when the
|
|
@@ -68,14 +69,91 @@ const CREATE_REGEX = /^\s*(gh|glab)\s+(pr|mr|issue)\s+(create|new)\b/;
|
|
|
68
69
|
*/
|
|
69
70
|
const DEFAULT_ACK_PATH = '.orchestrator/runtime/templates-acknowledged.json';
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Ceiling (in characters) for the raw bash command echoed into the deny reason.
|
|
74
|
+
*
|
|
75
|
+
* ## Why this file needs its own bound at all (#919, follow-up to #906)
|
|
76
|
+
*
|
|
77
|
+
* `command` is the ONE attacker/agent-controlled term in the reason — every
|
|
78
|
+
* other line is fixed text or a repo-derived path list. Echoing it unbounded
|
|
79
|
+
* made this hook the shortest path to the failure mode #906 was repaired for:
|
|
80
|
+
* a reason large enough to push the stdout envelope past the 65 536-byte kernel
|
|
81
|
+
* pipe buffer, where a truncated envelope reads as "no decision" and the tool
|
|
82
|
+
* call is ALLOWED. `emitDeny`'s {@link DENY_REASON_MAX} clamp (16 000) now
|
|
83
|
+
* stands in that path, but a consumer that respects the bound itself is the
|
|
84
|
+
* more robust shape — defence in depth, not reliance on the single downstream
|
|
85
|
+
* clamp.
|
|
86
|
+
*
|
|
87
|
+
* The clamp alone is also NOT sufficient here, which is the concrete bug this
|
|
88
|
+
* constant fixes rather than merely hardens against. `Command:` is line 2 of 6;
|
|
89
|
+
* the template-path list and the `/templates-ack` hint are lines 3-6. A
|
|
90
|
+
* 200 000-char command therefore consumed the entire 16 000-char budget and cut
|
|
91
|
+
* the remedy off the end — the deny still bit, but PRD § 3 Gherkin Pattern 3's
|
|
92
|
+
* required content never reached the reader.
|
|
93
|
+
*
|
|
94
|
+
* ## Why 512 and not the 80 used at the two stderr sites below
|
|
95
|
+
*
|
|
96
|
+
* This file already truncates `command` twice (bypass-matched and
|
|
97
|
+
* no-templates-found), both at `slice(0, 80)`. Those are one-line **stderr log**
|
|
98
|
+
* lines, where 80 is the terminal-width convention — a different consumer class.
|
|
99
|
+
* The repo's precedent for bounding a bash command inside a **structured record
|
|
100
|
+
* field** is `hooks/pre-bash-staging-fence.mjs` (`staged_paths[].command`,
|
|
101
|
+
* `slice(0, 512)`), and `permissionDecisionReason` is exactly that: a structured
|
|
102
|
+
* field, not a log line. Reusing 512 follows the matching house convention
|
|
103
|
+
* instead of inventing a third number. Reusing 80 would clip a realistic
|
|
104
|
+
* `gh pr create --title … --body …` mid-flag, defeating the recognisability the
|
|
105
|
+
* line exists to provide.
|
|
106
|
+
*
|
|
107
|
+
* Bound check: the fixed part of this reason measures 348 chars (see the
|
|
108
|
+
* call-site table in `scripts/lib/io.mjs`), so the worst case is 348 + 512 +
|
|
109
|
+
* the path list — ~19× below `DENY_REASON_MAX` and ~76× below the pipe buffer.
|
|
110
|
+
* This hook no longer contributes an unbounded term to the envelope at all.
|
|
111
|
+
*/
|
|
112
|
+
const COMMAND_ECHO_MAX = 512;
|
|
113
|
+
|
|
71
114
|
// ---------------------------------------------------------------------------
|
|
72
115
|
// Helpers
|
|
73
116
|
// ---------------------------------------------------------------------------
|
|
74
117
|
|
|
75
118
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
119
|
+
* Clip the echoed command to {@link COMMAND_ECHO_MAX}, marking the cut so the
|
|
120
|
+
* reader can tell the command was truncated rather than ending there.
|
|
121
|
+
*
|
|
122
|
+
* The marker is budgeted INSIDE the ceiling — mirroring `_clampReason` in
|
|
123
|
+
* `scripts/lib/io.mjs` — so the returned string never exceeds it and the
|
|
124
|
+
* worst-case reason length stays a fixed, auditable number.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} command
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
function clipCommand(command) {
|
|
130
|
+
if (command.length <= COMMAND_ECHO_MAX) return command;
|
|
131
|
+
const marker = `… [truncated: showing ${COMMAND_ECHO_MAX} of ${command.length} characters]`;
|
|
132
|
+
return command.slice(0, COMMAND_ECHO_MAX - marker.length) + marker;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Block the create command: emit the PreToolUse deny envelope via emitDeny
|
|
137
|
+
* (exit 0). Mirrors pre-bash-destructive-guard.mjs blockCommand() so downstream
|
|
138
|
+
* Claude Code rendering is consistent.
|
|
139
|
+
*
|
|
140
|
+
* PRD § 3 Gherkin Pattern 3 requires the template-path list plus the
|
|
141
|
+
* `/templates-ack` hint to reach the reader. That requirement is UNCHANGED —
|
|
142
|
+
* only the channel moved (#906). Until #906 this text was written to stderr AND
|
|
143
|
+
* duplicated into an `exit 2` stdout envelope, the mixed form the hook docs
|
|
144
|
+
* forbid ("choose one approach per hook, not both"). Under exit 0, stderr is
|
|
145
|
+
* only surfaced in the debug log — invisible to both operator and model — so a
|
|
146
|
+
* stderr write would look alive while being dead. The full multi-line text
|
|
147
|
+
* therefore travels as the emitDeny `reason`, landing in
|
|
148
|
+
* `permissionDecisionReason`, which is fed to **Claude** — the actor that has
|
|
149
|
+
* to read the template or run `/templates-ack`. The operator sees the first
|
|
150
|
+
* line via the derived `systemMessage` headline.
|
|
151
|
+
*
|
|
152
|
+
* The echoed `command` is bounded by {@link COMMAND_ECHO_MAX} before it enters
|
|
153
|
+
* the reason: it is the only agent-controlled term here, and left unbounded it
|
|
154
|
+
* both risked the pipe-buffer fail-open #906 repaired AND pushed the template
|
|
155
|
+
* list plus the ack hint past `emitDeny`'s clamp — i.e. truncated away exactly
|
|
156
|
+
* the content the PRD requires. See that constant for the full rationale.
|
|
79
157
|
*
|
|
80
158
|
* @param {{ host: string, command: string, templatePaths: string[],
|
|
81
159
|
* ackFile: string }} ctx
|
|
@@ -88,21 +166,16 @@ function blockCreate(ctx) {
|
|
|
88
166
|
: ' (none configured)';
|
|
89
167
|
const reason = [
|
|
90
168
|
`pre-bash-templates-first: ${host} create call detected without prior template Read.`,
|
|
91
|
-
`Command: ${command}`,
|
|
169
|
+
`Command: ${clipCommand(command)}`,
|
|
92
170
|
`Found templates:`,
|
|
93
171
|
pathList,
|
|
94
172
|
`Read one of these first, OR run \`/templates-ack\` (writes ${ackFile}) to bypass for this session.`,
|
|
95
173
|
`See: issue #519, "gsd Pattern Adoption Quick-Wins" (archived in the private Meta-Vault) (Pattern 3)`,
|
|
96
174
|
].join('\n');
|
|
97
175
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
process.stderr.write(reason + '\n');
|
|
102
|
-
process.stdout.write(
|
|
103
|
-
JSON.stringify({ permissionDecision: 'deny', reason }) + '\n',
|
|
104
|
-
);
|
|
105
|
-
process.exit(2);
|
|
176
|
+
// Single channel: the full multi-line reason (template paths + ack hint)
|
|
177
|
+
// rides in permissionDecisionReason. Never returns.
|
|
178
|
+
emitDeny(reason);
|
|
106
179
|
}
|
|
107
180
|
|
|
108
181
|
/**
|
|
@@ -142,46 +215,6 @@ function resolvePolicyPath(projectDir) {
|
|
|
142
215
|
return null;
|
|
143
216
|
}
|
|
144
217
|
|
|
145
|
-
/**
|
|
146
|
-
* Determine which host the command targets by inspecting the CREATE_REGEX
|
|
147
|
-
* capture. `gh` → "github", `glab` → "gitlab".
|
|
148
|
-
*
|
|
149
|
-
* @param {string} command
|
|
150
|
-
* @returns {"github"|"gitlab"|null}
|
|
151
|
-
*/
|
|
152
|
-
function resolveHost(command) {
|
|
153
|
-
const m = command.match(CREATE_REGEX);
|
|
154
|
-
if (!m) return null;
|
|
155
|
-
return m[1] === 'gh' ? 'github' : 'gitlab';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* True when the command starts with any of the bypass patterns. Bypass match
|
|
160
|
-
* is a prefix check with a word/EOL boundary on the trailing edge — this
|
|
161
|
-
* prevents trivial bypass via prefix-inclusion (e.g. policy entry
|
|
162
|
-
* "gh issue create --label bot" must not match "gh issue create --label botanical").
|
|
163
|
-
*
|
|
164
|
-
* @param {string} command
|
|
165
|
-
* @param {string[]} bypassPatterns
|
|
166
|
-
* @returns {boolean}
|
|
167
|
-
*/
|
|
168
|
-
function matchesBypass(command, bypassPatterns) {
|
|
169
|
-
if (!Array.isArray(bypassPatterns) || bypassPatterns.length === 0) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
const stripped = command.replace(/^\s+/, '');
|
|
173
|
-
for (const pat of bypassPatterns) {
|
|
174
|
-
if (typeof pat !== 'string' || pat.length === 0) continue;
|
|
175
|
-
const patStripped = pat.replace(/^\s+/, '');
|
|
176
|
-
if (!stripped.startsWith(patStripped)) continue;
|
|
177
|
-
// Boundary check: next character must be whitespace, EOL, or absent.
|
|
178
|
-
// This prevents "gh foo --label bot" from matching policy "gh foo --label botanical".
|
|
179
|
-
const nextChar = stripped.charAt(patStripped.length);
|
|
180
|
-
if (nextChar === '' || /\s/.test(nextChar)) return true;
|
|
181
|
-
}
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
218
|
/**
|
|
186
219
|
* Check the acknowledgement file for the current session_id. Best-effort:
|
|
187
220
|
* any read or parse error means "no acknowledgement" (returns false) so the
|
|
@@ -6,23 +6,35 @@
|
|
|
6
6
|
* Fires when the Skill tool is invoked (a skill is selected). Writes a
|
|
7
7
|
* selection record to `.orchestrator/metrics/skill-invocations.jsonl`.
|
|
8
8
|
*
|
|
9
|
-
* Decision flow:
|
|
9
|
+
* Decision flow (when run as the hook, not imported):
|
|
10
10
|
* 1. shouldRunHook gate — exit 0 immediately when the hook is disabled.
|
|
11
11
|
* 2. Read JSON payload from stdin: { tool_name, tool_input: { skill }, session_id }.
|
|
12
12
|
* 3. Belt-and-suspenders guard: if tool_name !== "Skill", exit 0 immediately.
|
|
13
13
|
* 4. Build a 'selected' record and call appendSkillInvocation().
|
|
14
|
-
* 5.
|
|
14
|
+
* 5. Daily-fallback telemetry flush check (Epic #841, #844) — non-blocking:
|
|
15
|
+
* when a bounded offline queue has aged past 24h AND consent resolves to
|
|
16
|
+
* send, spawn a detached child that runs `telemetry _flush`. Cheap by
|
|
17
|
+
* construction (env kill-switch pre-check → queue+state stat → consent)
|
|
18
|
+
* and never loads the roster on the hot hook path.
|
|
19
|
+
* 6. Output: nothing on stdout. Diagnostic errors to stderr only.
|
|
15
20
|
*
|
|
16
21
|
* Exit codes: 0 always (informational, never blocking).
|
|
22
|
+
*
|
|
23
|
+
* The module is import-safe: the self-execution block below is guarded so that a
|
|
24
|
+
* test can `import { maybeSpawnDailyFlush }` without triggering the hook's
|
|
25
|
+
* stdin-read + exit path.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
|
-
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
20
|
-
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
21
|
-
if (!shouldRunHook('skill-invocation-telemetry')) process.exit(0);
|
|
22
|
-
|
|
23
28
|
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { spawn } from 'node:child_process';
|
|
31
|
+
|
|
32
|
+
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
24
33
|
import { appendSkillInvocation } from '../scripts/lib/skill-invocations-schema.mjs';
|
|
25
34
|
import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
35
|
+
import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
|
|
36
|
+
import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
|
|
37
|
+
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
26
38
|
|
|
27
39
|
// ---------------------------------------------------------------------------
|
|
28
40
|
// Constants
|
|
@@ -30,10 +42,29 @@ import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
|
30
42
|
|
|
31
43
|
const JSONL_PATH = path.join(SO_PROJECT_DIR, '.orchestrator', 'metrics', 'skill-invocations.jsonl');
|
|
32
44
|
|
|
45
|
+
/** Absolute path to the telemetry CLI (carries the hidden `_flush` subcommand). */
|
|
46
|
+
const TELEMETRY_CLI_PATH = path.resolve(
|
|
47
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
48
|
+
'../scripts/telemetry.mjs',
|
|
49
|
+
);
|
|
50
|
+
|
|
33
51
|
// ---------------------------------------------------------------------------
|
|
34
52
|
// Helpers
|
|
35
53
|
// ---------------------------------------------------------------------------
|
|
36
54
|
|
|
55
|
+
/**
|
|
56
|
+
* True when an env var carries a truthy "on" signal (present, non-empty, not
|
|
57
|
+
* '0'/'false'). Kept local so the hook does not pull anything extra from consent.mjs.
|
|
58
|
+
* @param {unknown} raw
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function isTruthyEnvFlag(raw) {
|
|
62
|
+
if (raw === undefined || raw === null) return false;
|
|
63
|
+
const t = String(raw).trim();
|
|
64
|
+
if (t === '' || t === '0') return false;
|
|
65
|
+
return t.toLowerCase() !== 'false';
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
/**
|
|
38
69
|
* Read stdin to EOF (best-effort). Returns parsed JSON or null on failure.
|
|
39
70
|
* Uses a 5 s timeout consistent with Claude Code hook contract.
|
|
@@ -61,6 +92,61 @@ function readStdinJson() {
|
|
|
61
92
|
});
|
|
62
93
|
}
|
|
63
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Non-blocking daily-fallback flush trigger. Ordered cheapest-first:
|
|
97
|
+
* 1. env kill-switches (no I/O) — DO_NOT_TRACK / SO_TELEMETRY_DISABLED.
|
|
98
|
+
* 2. shouldDailyFlush (one telemetry.json read + one queue stat) — the common
|
|
99
|
+
* case (empty queue) returns here WITHOUT loading owner.yaml or the roster.
|
|
100
|
+
* 3. full consent resolution — spawn a detached `telemetry _flush` only when it
|
|
101
|
+
* resolves to send.
|
|
102
|
+
*
|
|
103
|
+
* Never throws; the caller's hook must always exit 0.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} [opts]
|
|
106
|
+
* @param {NodeJS.ProcessEnv} [opts.env] Env source (default process.env).
|
|
107
|
+
* @param {typeof spawn} [opts.spawnFn] Spawn function (test injection).
|
|
108
|
+
* @param {number} [opts.now] Reference time epoch-ms (default Date.now()).
|
|
109
|
+
* @param {string} [opts.statePath] telemetry.json path override (test injection).
|
|
110
|
+
* @param {string} [opts.queuePath] queue path override (test injection).
|
|
111
|
+
* @returns {{ spawned: boolean, reason: string }}
|
|
112
|
+
*/
|
|
113
|
+
export function maybeSpawnDailyFlush({
|
|
114
|
+
env = process.env,
|
|
115
|
+
spawnFn = spawn,
|
|
116
|
+
now = Date.now(),
|
|
117
|
+
statePath,
|
|
118
|
+
queuePath,
|
|
119
|
+
} = {}) {
|
|
120
|
+
try {
|
|
121
|
+
// 1. Cheapest gate: env kill-switches, no file I/O.
|
|
122
|
+
if (isTruthyEnvFlag(env?.DO_NOT_TRACK) || env?.SO_TELEMETRY_DISABLED === '1') {
|
|
123
|
+
return { spawned: false, reason: 'disabled-env' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. Cheap backlog check — bails out before owner.yaml load in the common case.
|
|
127
|
+
if (!shouldDailyFlush({ statePath, queuePath, now })) {
|
|
128
|
+
return { spawned: false, reason: 'not-due' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Authoritative consent gate.
|
|
132
|
+
const ownerConfig = loadOwnerConfig().config;
|
|
133
|
+
const { record } = readTelemetryState({ path: statePath });
|
|
134
|
+
const consent = resolveConsent({ env, ownerConfig, state: record, interactive: false });
|
|
135
|
+
if (consent.send !== true) {
|
|
136
|
+
return { spawned: false, reason: 'gated' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const child = spawnFn(process.execPath, [TELEMETRY_CLI_PATH, '_flush'], {
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: 'ignore',
|
|
142
|
+
});
|
|
143
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
144
|
+
return { spawned: true, reason: 'spawned' };
|
|
145
|
+
} catch {
|
|
146
|
+
return { spawned: false, reason: 'error' };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
64
150
|
// ---------------------------------------------------------------------------
|
|
65
151
|
// Main
|
|
66
152
|
// ---------------------------------------------------------------------------
|
|
@@ -91,9 +177,22 @@ async function main() {
|
|
|
91
177
|
};
|
|
92
178
|
|
|
93
179
|
await appendSkillInvocation(JSONL_PATH, record);
|
|
180
|
+
|
|
181
|
+
// Daily-fallback telemetry flush — non-blocking, best-effort, never throws.
|
|
182
|
+
maybeSpawnDailyFlush();
|
|
94
183
|
}
|
|
95
184
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Self-execution guard — run only when invoked directly (not when imported).
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
|
|
190
|
+
if (isMain) {
|
|
191
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
192
|
+
if (!shouldRunHook('skill-invocation-telemetry')) process.exit(0);
|
|
193
|
+
|
|
194
|
+
// Exit 0 always — informational hook must never block the Skill tool.
|
|
195
|
+
main().catch((err) => {
|
|
196
|
+
process.stderr.write(`[skill-invocation-telemetry] ERROR: ${err?.message ?? err}\n`);
|
|
197
|
+
}).finally(() => process.exit(0));
|
|
198
|
+
}
|