session-orchestrator 5.0.0 → 5.2.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/autopilot/SKILL.md +1 -0
- package/.agents/skills/bootstrap/SKILL.md +2 -0
- package/.agents/skills/brainstorm/SKILL.md +3 -0
- package/.agents/skills/close/SKILL.md +17 -0
- package/.agents/skills/debug/SKILL.md +2 -0
- package/.agents/skills/discovery/SKILL.md +2 -1
- package/.agents/skills/dispatcher/SKILL.md +2 -0
- package/.agents/skills/eli5/SKILL.md +2 -0
- package/.agents/skills/eval/SKILL.md +1 -0
- package/.agents/skills/evolve/SKILL.md +2 -1
- package/.agents/skills/go/SKILL.md +18 -0
- package/.agents/skills/grill/SKILL.md +2 -0
- package/.agents/skills/harness-audit/SKILL.md +16 -0
- package/.agents/skills/memory-cleanup/SKILL.md +1 -0
- package/.agents/skills/persona-panel/SKILL.md +1 -0
- package/.agents/skills/plan/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +17 -0
- package/.agents/skills/reconcile/SKILL.md +1 -0
- package/.agents/skills/release/SKILL.md +18 -0
- package/.agents/skills/repo-audit/SKILL.md +1 -0
- package/.agents/skills/spinout/SKILL.md +1 -0
- package/.agents/skills/sunset-review/SKILL.md +2 -0
- package/.agents/skills/test/SKILL.md +17 -0
- package/.agents/skills/ux-grill/SKILL.md +2 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/convergence-monitoring.md +13 -0
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/npm-publish.md +13 -0
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +2 -2
- package/.orchestrator/policy/blocked-commands.json +10 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +74 -235
- package/commands/session.md +10 -0
- package/docs/USER-GUIDE.md +24 -0
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +12 -5
- package/docs/events-schema.md +5 -1
- package/docs/install.md +128 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +83 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +6 -4
- package/docs/session-config-template.md +38 -0
- package/docs/telemetry.md +15 -0
- package/hooks/_lib/hook-import-set.json +46 -6
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/enforce-scope.mjs +42 -1
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +14 -2
- package/hooks/on-stop.mjs +43 -1
- package/hooks/post-bash-write-verify.mjs +3 -0
- package/hooks/pre-auq-clarity.mjs +3 -0
- package/hooks/pre-bash-issue-budget.mjs +103 -17
- package/hooks/pre-task-scope-disjoint.mjs +152 -3
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/package.json +3 -2
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/convergence-monitoring.md +11 -0
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/npm-publish.md +11 -0
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/backfill-abandoned-sessions.mjs +57 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/emit-session.mjs +2 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +173 -9
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +209 -9
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/convergence-monitor.mjs +2 -2
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/instruction-budget-guard.mjs +186 -46
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +69 -3
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +63 -2
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +392 -7
- package/scripts/lib/session-close-backfill.mjs +58 -6
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
- package/scripts/lib/user-invocable-skills.mjs +185 -0
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +366 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-skill-links.mjs +27 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
- package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
- package/scripts/lib/validate/check-unwired-features.mjs +266 -11
- package/scripts/lib/validate/check-validator-registration.mjs +2 -2
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate-vendored-rules.mjs +35 -9
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release.mjs +124 -35
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +216 -0
- package/scripts/validate-plugin.mjs +9 -0
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +2 -3
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +58 -4
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +14 -0
- package/skills/evolve/SKILL.md +8 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/skills/playwright-driver/SKILL.md +7 -10
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +10 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-plan/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +19 -1
- package/skills/wave-executor/SKILL.md +7 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
|
|
35
35
|
import { execFile as _execFile } from 'node:child_process';
|
|
36
36
|
import { promisify } from 'node:util';
|
|
37
|
-
import { pathToFileURL } from 'node:url';
|
|
38
37
|
import { resolveRepoSpec as _resolveRepoSpec, resolveRepoHost as _resolveRepoHost } from './lib/vcs-repo-spec.mjs';
|
|
38
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
39
39
|
|
|
40
40
|
const execFileAsync = promisify(_execFile);
|
|
41
41
|
|
|
@@ -347,8 +347,7 @@ export async function auditGithubBranchProtection(opts = {}, deps = {}) {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
// CLI entry — only when run directly, not when imported (e.g. by tests).
|
|
350
|
-
const isMain =
|
|
351
|
-
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
350
|
+
const isMain =isMainModule(import.meta.url);
|
|
352
351
|
|
|
353
352
|
if (isMain) {
|
|
354
353
|
auditGithubBranchProtection({ repoRoot: process.cwd() }).then((result) => {
|
|
@@ -58,7 +58,12 @@ export function parseAgentFrontmatter(contents) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// A UTF-8 BOM before `---` makes FRONTMATTER_RE miss the block entirely, so a
|
|
62
|
+
// BOM-prefixed SKILL.md would parse as "no frontmatter" and every flag in it
|
|
63
|
+
// (`user-invocable` included) silently disappear.
|
|
64
|
+
const text = contents.charCodeAt(0) === 0xfeff ? contents.slice(1) : contents;
|
|
65
|
+
|
|
66
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
62
67
|
if (!match) {
|
|
63
68
|
return {
|
|
64
69
|
ok: false,
|
|
@@ -76,19 +81,36 @@ export function parseAgentFrontmatter(contents) {
|
|
|
76
81
|
const frontmatter = /** @type {Record<string,string>} */ ({});
|
|
77
82
|
const lines = fmText.split(/\r?\n/);
|
|
78
83
|
|
|
84
|
+
// Indented lines belong to whatever the preceding top-level key opened (a
|
|
85
|
+
// nested mapping, or a block scalar) — they are NOT top-level keys. Without
|
|
86
|
+
// this, `metadata:\n user-invocable: true` was HOISTED to top level and read
|
|
87
|
+
// as the slash-command marker, so the counters saw a skill no generator did.
|
|
88
|
+
let inNestedBlock = false;
|
|
89
|
+
|
|
79
90
|
for (const rawLine of lines) {
|
|
80
91
|
const line = rawLine.replace(/\s+$/, '');
|
|
81
92
|
if (line === '' || /^\s*#/.test(line)) continue;
|
|
82
93
|
|
|
94
|
+
if (/^\s/.test(rawLine)) {
|
|
95
|
+
if (inNestedBlock) continue;
|
|
96
|
+
} else {
|
|
97
|
+
inNestedBlock = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
// Detect block-scalar indicators on a key line, e.g. `description: >` or `description: |`
|
|
84
101
|
// These are valid YAML but break Claude Code's parser.
|
|
85
102
|
const blockScalarMatch = /^(\w[\w-]*):\s*[>|]\s*$/.exec(line);
|
|
86
103
|
if (blockScalarMatch) {
|
|
87
104
|
// Store a sentinel so the validator can emit the correct rule.
|
|
88
105
|
frontmatter[blockScalarMatch[1]] = '__BLOCK_SCALAR__';
|
|
106
|
+
inNestedBlock = true;
|
|
89
107
|
continue;
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
// A bare `key:` at top level opens a nested mapping (or an empty value);
|
|
111
|
+
// everything indented under it is that mapping's business, not ours.
|
|
112
|
+
if (/^\w[\w-]*:$/.test(line)) inNestedBlock = true;
|
|
113
|
+
|
|
92
114
|
const idx = line.indexOf(':');
|
|
93
115
|
if (idx === -1) continue;
|
|
94
116
|
const key = line.slice(0, idx).trim();
|
|
@@ -67,6 +67,7 @@ import { basename, resolve, sep } from 'node:path';
|
|
|
67
67
|
import { homedir } from 'node:os';
|
|
68
68
|
import { resolveInstructionFile } from './common.mjs';
|
|
69
69
|
import { isSessionConfigHeading } from './config/section-extractor.mjs';
|
|
70
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
70
71
|
|
|
71
72
|
/** Thrown by `lintClaudeMd()` on any infra-level failure (missing/unreadable file). */
|
|
72
73
|
export class ClaudeMdLintInfraError extends Error {
|
|
@@ -536,10 +537,6 @@ function main() {
|
|
|
536
537
|
}
|
|
537
538
|
|
|
538
539
|
const __filename = fileURLToPath(import.meta.url);
|
|
539
|
-
const isMain =
|
|
540
|
-
typeof process !== 'undefined' &&
|
|
541
|
-
process.argv[1] !== null &&
|
|
542
|
-
process.argv[1] !== undefined &&
|
|
543
|
-
resolve(process.argv[1]) === resolve(__filename);
|
|
540
|
+
const isMain =isMainModule(import.meta.url);
|
|
544
541
|
|
|
545
542
|
if (isMain) main();
|
|
@@ -743,6 +743,31 @@ export { splitSegments as splitChainSegments };
|
|
|
743
743
|
|
|
744
744
|
const ENV_ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
745
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Verbs that name a WRITE TARGET as a bare operand — no redirect operator, no
|
|
748
|
+
* wrapper flag (#1366). A sibling of WRAPPER_UNWRAP's `fileArgFlags`: that table
|
|
749
|
+
* answers "does this WRAPPER flag's operand name a file the wrapper writes?",
|
|
750
|
+
* this one "does this VERB's operand name a file the verb writes?".
|
|
751
|
+
*
|
|
752
|
+
* Spec fields:
|
|
753
|
+
* - prefix: the operand's literal prefix (`of=`), matched on a token AFTER
|
|
754
|
+
* the resolved verb.
|
|
755
|
+
* - mode: the default redirect mode the write is equivalent to.
|
|
756
|
+
* - appendRe: operands that turn the write into an APPEND. `dd` truncates by
|
|
757
|
+
* default (`conv=notrunc` / `oflag=append` do not), so reporting
|
|
758
|
+
* `truncate` unconditionally would over-report a rule class that
|
|
759
|
+
* deliberately permits `>>`.
|
|
760
|
+
*
|
|
761
|
+
* Deliberately narrow — one entry, measured. `tee`/`sponge` and in-process
|
|
762
|
+
* writers (`node -e fs.writeFileSync`) are NOT here: they are not redirect-shaped
|
|
763
|
+
* and belong to a different rule class (see the `redirect-harness-memory`
|
|
764
|
+
* rationale in .orchestrator/policy/blocked-commands.json). REVISIT TRIGGER: a
|
|
765
|
+
* second `<verb> <prefix>file` writer showing up in a transcript census.
|
|
766
|
+
*/
|
|
767
|
+
const VERB_FILE_OPERANDS = new Map([
|
|
768
|
+
['dd', { prefix: 'of=', mode: 'truncate', appendRe: /^(?:conv=.*\bnotrunc\b|oflag=.*\bappend\b)/ }],
|
|
769
|
+
]);
|
|
770
|
+
|
|
746
771
|
/**
|
|
747
772
|
* Transparent process-wrapper table for verb resolution (#982), keyed by
|
|
748
773
|
* basename. Each spec describes how to skip a wrapper's own options so the
|
|
@@ -946,15 +971,37 @@ function resolveCore(segment, unknownFlagsTakeValue) {
|
|
|
946
971
|
if (spec.splitString
|
|
947
972
|
&& (text === '-S' || text === '--split-string'
|
|
948
973
|
|| text.startsWith('--split-string=') || text.startsWith('-S'))) {
|
|
974
|
+
// env(1) APPENDS the remaining argv to the split string: `env -S bash
|
|
975
|
+
// -c '…'` runs `bash -c '…'`, not `bash` (#1366). Collecting only the
|
|
976
|
+
// operand made the trailing `-c '…'` execute unseen — the segment then
|
|
977
|
+
// resolved to `verb: 'echo x > CLAUDE.md'` (the quoted payload read as
|
|
978
|
+
// the verb) and neither the match surface nor the redirect surface ever
|
|
979
|
+
// saw it. So the payload is the operand PLUS every remaining token, and
|
|
980
|
+
// the wrapper CONSUMES the segment: nothing is left to resolve as a
|
|
981
|
+
// verb, which is why this now yields `verb: null, index: -1`.
|
|
982
|
+
let operand = null;
|
|
949
983
|
if (text.startsWith('--split-string=')) {
|
|
950
|
-
|
|
984
|
+
operand = text.slice('--split-string='.length);
|
|
985
|
+
i++;
|
|
951
986
|
} else if (text !== '-S' && text !== '--split-string') {
|
|
952
|
-
|
|
953
|
-
} else if (i + 1 < segment.length) {
|
|
954
|
-
payloads.push(segment[i + 1].text);
|
|
987
|
+
operand = text.slice(2); // attached form: -S'string'
|
|
955
988
|
i++;
|
|
989
|
+
} else if (i + 1 < segment.length) {
|
|
990
|
+
operand = segment[i + 1].text;
|
|
991
|
+
i += 2;
|
|
992
|
+
} else {
|
|
993
|
+
i++; // `env -S` with no operand at all — nothing to collect
|
|
994
|
+
}
|
|
995
|
+
if (operand !== null) {
|
|
996
|
+
// Raw token texts, joined by one space. Quoting is NOT re-applied:
|
|
997
|
+
// the tokenizer already stripped it, and re-inventing it would be a
|
|
998
|
+
// second quoting grammar. The direction of that loss is safe — an
|
|
999
|
+
// over-wide payload can only make MORE text visible to the matcher
|
|
1000
|
+
// (fail-closed), never hide a redirect operator.
|
|
1001
|
+
const rest = segment.slice(i).map((t) => t.text);
|
|
1002
|
+
payloads.push(rest.length > 0 ? [operand, ...rest].join(' ') : operand);
|
|
1003
|
+
i = segment.length;
|
|
956
1004
|
}
|
|
957
|
-
i++;
|
|
958
1005
|
continue;
|
|
959
1006
|
}
|
|
960
1007
|
if (spec.shellFlags && spec.shellFlags.has(text)) { sawShellFlag = true; i++; continue; }
|
|
@@ -1189,15 +1236,65 @@ function dedupedSegmentPayloads(segment, resolved) {
|
|
|
1189
1236
|
if (resolved.verb && DASH_C_SHELLS.has(resolved.verb)) {
|
|
1190
1237
|
for (const p of dashCPayloads(segment, resolved.index)) payloadSet.add(p);
|
|
1191
1238
|
}
|
|
1239
|
+
addArgJoinedPayload(segment, resolved.verb, resolved.index, payloadSet);
|
|
1192
1240
|
if (resolved.alt) {
|
|
1193
1241
|
for (const p of resolved.alt.payloads) payloadSet.add(p);
|
|
1194
1242
|
if (resolved.alt.verb && DASH_C_SHELLS.has(resolved.alt.verb)) {
|
|
1195
1243
|
for (const p of dashCPayloads(segment, resolved.alt.index)) payloadSet.add(p);
|
|
1196
1244
|
}
|
|
1245
|
+
addArgJoinedPayload(segment, resolved.alt.verb, resolved.alt.index, payloadSet);
|
|
1197
1246
|
}
|
|
1198
1247
|
return [...payloadSet];
|
|
1199
1248
|
}
|
|
1200
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* Interpreters that JOIN their remaining arguments with a space and execute the
|
|
1252
|
+
* result — no `-c` flag, no wrapper option to key on (#1366).
|
|
1253
|
+
*
|
|
1254
|
+
* `eval` is the whole set today. It was already in SHELL_EXEC_INTERPRETERS, but
|
|
1255
|
+
* that set only powers the QUOTED-TOKEN match in matchSegments — it never fed
|
|
1256
|
+
* the payload traversal, so `eval 'echo x > CLAUDE.md'` carried a redirect no
|
|
1257
|
+
* redirect rule ever saw (`resolveSegmentVerb` reports `payloads: []` for it).
|
|
1258
|
+
*
|
|
1259
|
+
* NAMED CEILING (BV-004): only a LITERAL payload is reachable. `eval "$CMD"` and
|
|
1260
|
+
* `eval $(…)` do not exist as command text at hook time and stay in the existing
|
|
1261
|
+
* `unresolved` class (#641). REVISIT TRIGGER: a second arg-joining interpreter
|
|
1262
|
+
* showing up in a transcript census — extend the set, never special-case `eval`.
|
|
1263
|
+
*/
|
|
1264
|
+
const ARG_JOINING_INTERPRETERS = new Set(['eval']);
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Add the joined post-verb argument text as a recursion payload when `verb` is
|
|
1268
|
+
* an arg-joining interpreter. No-op otherwise — every other caller shape keeps
|
|
1269
|
+
* the pre-#1366 payload set byte-identical.
|
|
1270
|
+
*
|
|
1271
|
+
* Quoting is not re-applied for the same reason as the `env -S` tail (see
|
|
1272
|
+
* resolveCore): the tokenizer stripped it, and a wider payload can only make
|
|
1273
|
+
* more text visible to the matcher, never hide an operator.
|
|
1274
|
+
*
|
|
1275
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
1276
|
+
* @param {string|null} verb
|
|
1277
|
+
* @param {number} index — the verb's token index (`-1` when unresolved)
|
|
1278
|
+
* @param {Set<string>} payloadSet — mutated in place
|
|
1279
|
+
*/
|
|
1280
|
+
function addArgJoinedPayload(segment, verb, index, payloadSet) {
|
|
1281
|
+
if (!verb || index < 0 || !ARG_JOINING_INTERPRETERS.has(verb)) return;
|
|
1282
|
+
const words = [];
|
|
1283
|
+
for (let i = index + 1; i < segment.length; i++) {
|
|
1284
|
+
if (segment[i].redirect) {
|
|
1285
|
+
// A redirect belongs to the SEGMENT (`eval 'x' > f` redirects eval's own
|
|
1286
|
+
// stdout), never to the joined argv — the segment scan already reported
|
|
1287
|
+
// it, and re-reporting it through the payload would duplicate the entry.
|
|
1288
|
+
const end = redirectSpanEnd(segment, i);
|
|
1289
|
+
if (end > i) i = end;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
words.push(segment[i].text);
|
|
1293
|
+
}
|
|
1294
|
+
const joined = words.join(' ').trim();
|
|
1295
|
+
if (joined.length > 0) payloadSet.add(joined);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1201
1298
|
/**
|
|
1202
1299
|
* Match a blocked-pattern regex against tokenized chain segments — the shared
|
|
1203
1300
|
* core of commandMatchesBlocked, recursion-capable for `-c` payloads (#982).
|
|
@@ -1347,6 +1444,37 @@ function collectRedirectTargets(segments, out, depth, budget) {
|
|
|
1347
1444
|
out.push({ target: wa.value, mode: 'truncate', fd: null });
|
|
1348
1445
|
}
|
|
1349
1446
|
|
|
1447
|
+
// A VERB can name its write target as a bare operand, with no redirect
|
|
1448
|
+
// operator and no wrapper flag: `dd of=CLAUDE.md` truncates CLAUDE.md while
|
|
1449
|
+
// `> CLAUDE.md` is denied (measured ALLOW pre-#1366 against the real
|
|
1450
|
+
// policy). Keyed on the RESOLVED verb, so `sudo dd of=X` is reached through
|
|
1451
|
+
// the existing wrapper unwrap; both readings contribute, as above.
|
|
1452
|
+
const seenVerbOperands = new Set();
|
|
1453
|
+
for (const reading of resolved.alt ? [resolved, resolved.alt] : [resolved]) {
|
|
1454
|
+
const spec = reading.verb ? VERB_FILE_OPERANDS.get(reading.verb) : null;
|
|
1455
|
+
if (!spec || reading.index < 0) continue;
|
|
1456
|
+
let operand = null;
|
|
1457
|
+
let append = false;
|
|
1458
|
+
for (let k = reading.index + 1; k < segment.length; k++) {
|
|
1459
|
+
const text = segment[k].text;
|
|
1460
|
+
if (segment[k].redirect) continue;
|
|
1461
|
+
if (text.startsWith(spec.prefix)) operand = text.slice(spec.prefix.length);
|
|
1462
|
+
else if (spec.appendRe?.test(text)) append = true;
|
|
1463
|
+
}
|
|
1464
|
+
if (operand === null || operand.length === 0) continue;
|
|
1465
|
+
const mode = append ? 'append' : spec.mode;
|
|
1466
|
+
const key = `${operand}:${mode}`;
|
|
1467
|
+
if (seenVerbOperands.has(key)) continue;
|
|
1468
|
+
seenVerbOperands.add(key);
|
|
1469
|
+
if (/[$`]/.test(operand)) {
|
|
1470
|
+
// Same fail-visible rule as a redirect operand (#983) — never guess at a
|
|
1471
|
+
// variable or a command substitution, never silently drop it either.
|
|
1472
|
+
out.push({ target: null, mode, fd: null, unresolved: true });
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
out.push({ target: operand, mode, fd: null });
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1350
1478
|
// Both readings of an ambiguous unknown flag contribute payloads (#1000),
|
|
1351
1479
|
// UNIONED and de-duped (HIGH-2). Walking parse A only let
|
|
1352
1480
|
// `env -Q x bash -c 'echo pwned > CLAUDE.md'` — which resolves to the
|
|
@@ -1663,18 +1791,90 @@ export function redirectRuleMatches(rule, command, opts = {}) {
|
|
|
1663
1791
|
const denylist = Array.isArray(rule['target-denylist']) ? rule['target-denylist'] : [];
|
|
1664
1792
|
if (denylist.length === 0) return false;
|
|
1665
1793
|
const modes = new Set(Array.isArray(rule.modes) && rule.modes.length > 0 ? rule.modes : ['truncate']);
|
|
1666
|
-
|
|
1794
|
+
|
|
1795
|
+
// #1362 — TWO denylist classes, deliberately kept apart:
|
|
1796
|
+
//
|
|
1797
|
+
// REPO-RELATIVE (every pre-#1362 entry): judged against
|
|
1798
|
+
// `repoRelativeRedirectTarget`, which returns `null` for anything outside
|
|
1799
|
+
// the root. Behaviour is byte-identical to before, and that null is what
|
|
1800
|
+
// keeps a relative entry from ever reaching an out-of-repo path.
|
|
1801
|
+
//
|
|
1802
|
+
// HOME-ANCHORED (`~/…`, new): judged against the EXPANDED ABSOLUTE target,
|
|
1803
|
+
// never relativized. Needed because the artefacts worth denying are not all
|
|
1804
|
+
// inside the repo — the harness auto-memory directory
|
|
1805
|
+
// (`~/.claude/projects/<encodeProjectDir(root)>/memory/`) is auto-injected
|
|
1806
|
+
// as trusted project context into every later session yet lives outside
|
|
1807
|
+
// `git diff`, CI review and the secret scanners. Measured 2026-09-13: with
|
|
1808
|
+
// only the repo-relative class, NO glob spelling could match it, because
|
|
1809
|
+
// `relativizeAgainstRoot` discards out-of-repo targets by design.
|
|
1810
|
+
//
|
|
1811
|
+
// The split is one-directional on purpose: a home-anchored entry cannot widen
|
|
1812
|
+
// an existing repo-relative rule, and a repo-relative entry still cannot
|
|
1813
|
+
// escape the root. An unexpandable `~` (no usable `home`) matches NOTHING —
|
|
1814
|
+
// fail-closed for the new class, since an unexpanded `~` names no file.
|
|
1815
|
+
const relRegexes = [];
|
|
1816
|
+
const absRegexes = [];
|
|
1817
|
+
for (const pattern of denylist) {
|
|
1818
|
+
const text = String(pattern);
|
|
1819
|
+
if (text === '~' || text.startsWith('~/')) {
|
|
1820
|
+
const expanded = expandLeadingHome(text, home);
|
|
1821
|
+
if (expanded === text) continue; // no usable home → no match, never a guess
|
|
1822
|
+
absRegexes.push(redirectGlobToRegExp(foldAbsoluteTarget(expanded)));
|
|
1823
|
+
} else {
|
|
1824
|
+
relRegexes.push(redirectGlobToRegExp(text));
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (relRegexes.length === 0 && absRegexes.length === 0) return false;
|
|
1667
1828
|
|
|
1668
1829
|
for (const entry of extractRedirectTargets(command)) {
|
|
1669
1830
|
if (entry.unresolved) continue;
|
|
1670
1831
|
if (!modes.has(entry.mode)) continue;
|
|
1671
|
-
|
|
1672
|
-
if (
|
|
1673
|
-
|
|
1832
|
+
|
|
1833
|
+
if (absRegexes.length > 0) {
|
|
1834
|
+
// A RELATIVE target is repo-root-relative here, exactly as the repo-relative
|
|
1835
|
+
// class already treats it (#994 R1) — otherwise `> ../../.claude/projects/
|
|
1836
|
+
// <enc>/memory/MEMORY.md` from the repo root names the very file the `~`
|
|
1837
|
+
// spelling blocks, and only the spelling would be denied (measured
|
|
1838
|
+
// 2026-09-13: relative `>`/`>>` ALLOW vs tilde/absolute BLOCK on one and
|
|
1839
|
+
// the same resolved path). This resolves TARGETS, never ENTRIES: a
|
|
1840
|
+
// repo-relative denylist entry keeps going through
|
|
1841
|
+
// `repoRelativeRedirectTarget` below and still cannot reach out of the root.
|
|
1842
|
+
const expandedTarget = expandLeadingHome(entry.target, home);
|
|
1843
|
+
const absTarget = path.isAbsolute(expandedTarget)
|
|
1844
|
+
? expandedTarget
|
|
1845
|
+
: repoRoot && path.isAbsolute(repoRoot)
|
|
1846
|
+
? path.resolve(repoRoot, expandedTarget)
|
|
1847
|
+
: null;
|
|
1848
|
+
if (absTarget !== null) {
|
|
1849
|
+
const folded = foldAbsoluteTarget(absTarget);
|
|
1850
|
+
if (absRegexes.some((re) => re.test(folded))) return true;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (relRegexes.length > 0) {
|
|
1855
|
+
const target = repoRelativeRedirectTarget(entry.target, repoRoot, home);
|
|
1856
|
+
if (target !== null && relRegexes.some((re) => re.test(target))) return true;
|
|
1857
|
+
}
|
|
1674
1858
|
}
|
|
1675
1859
|
return false;
|
|
1676
1860
|
}
|
|
1677
1861
|
|
|
1862
|
+
/**
|
|
1863
|
+
* Normalize an ABSOLUTE path for home-anchored denylist comparison (#1362):
|
|
1864
|
+
* same alias-collapse and case-fold `relativizeAgainstRoot` applies before its
|
|
1865
|
+
* containment test, so the two classes agree on what `/private/tmp` and a
|
|
1866
|
+
* case-insensitive volume mean. Applied to BOTH sides (pattern and target), so
|
|
1867
|
+
* the folding can never make a pattern match more than its literal spelling.
|
|
1868
|
+
*
|
|
1869
|
+
* @param {string} abs
|
|
1870
|
+
* @returns {string}
|
|
1871
|
+
*/
|
|
1872
|
+
function foldAbsoluteTarget(abs) {
|
|
1873
|
+
return stripPathAliases(path.posix.normalize(String(abs).replace(/\\/g, '/'))).toLocaleLowerCase(
|
|
1874
|
+
'en-US',
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1678
1878
|
/**
|
|
1679
1879
|
* Test whether a command string contains a blocked pattern with shell-aware
|
|
1680
1880
|
* boundaries AND a quoted-payload guard (#641).
|
|
@@ -5,6 +5,25 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
5
5
|
* drift-check.mjs — Parser for the top-level `drift-check:` YAML block.
|
|
6
6
|
*
|
|
7
7
|
* Ported from config-yaml-parser.sh (v2).
|
|
8
|
+
*
|
|
9
|
+
* SCOPE NOTE — this file parses FLAGS ONLY; it implements no check. The
|
|
10
|
+
* `check-command-count` flag below toggles the `command-count` surface in
|
|
11
|
+
* `skills/claude-md-drift-check/checker.mjs`, which is where the counting lives.
|
|
12
|
+
*
|
|
13
|
+
* `command-count` STAYS on `ls commands/*.md | wc -l` on purpose. The checker is
|
|
14
|
+
* GENERIC over any consumer repo, where a slash command still IS a
|
|
15
|
+
* `commands/*.md` file. The 2026-09-16 command→skill fold (#1370) is a fact
|
|
16
|
+
* about THIS plugin — 24 command files folded into their same-named
|
|
17
|
+
* `skills/<name>/SKILL.md` behind an explicit `user-invocable: true` — so
|
|
18
|
+
* teaching the shared checker that union would mis-measure every repo that never
|
|
19
|
+
* folded. `scripts/site-numbers.mjs` `countCommands()` carries the union for this
|
|
20
|
+
* plugin's own tile, and names the divergence in its docblock.
|
|
21
|
+
*
|
|
22
|
+
* CONSEQUENCE for THIS repo, left deliberately visible rather than papered over:
|
|
23
|
+
* a prose claim of the shape "N commands" / "N slash commands" in a
|
|
24
|
+
* drift-check-scanned doc is compared against the `commands/` listing (2 after
|
|
25
|
+
* the fold), so such a claim must either be dropped or reworded off the
|
|
26
|
+
* `\b(\d+)\s+(?:\/)?commands?\b` pattern the checker matches.
|
|
8
27
|
*/
|
|
9
28
|
|
|
10
29
|
/**
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
47
47
|
import { join } from 'node:path';
|
|
48
|
-
import {
|
|
48
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
49
49
|
|
|
50
50
|
const DEFAULT_INTERVAL_S = 2;
|
|
51
51
|
const EVENTS_FILE_REL = '.orchestrator/metrics/events.jsonl';
|
|
@@ -502,7 +502,7 @@ function main() {
|
|
|
502
502
|
|
|
503
503
|
// Run the tail loop only when executed as a script. Importing the module (for
|
|
504
504
|
// unit tests over `classify`) must not parse vitest's argv and exit 1.
|
|
505
|
-
if (
|
|
505
|
+
if (isMainModule(import.meta.url)) {
|
|
506
506
|
main();
|
|
507
507
|
}
|
|
508
508
|
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
-
import { pathToFileURL } from 'node:url';
|
|
22
21
|
|
|
23
22
|
import { matcherMatches, readHookDecision } from './pi-hook-bridge.mjs';
|
|
24
23
|
import { resolvePluginRoot } from './plugin-root.mjs';
|
|
24
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
25
25
|
|
|
26
26
|
export const CURSOR_TO_CANONICAL_EVENT = Object.freeze({
|
|
27
27
|
sessionStart: 'SessionStart',
|
|
@@ -434,7 +434,7 @@ async function main() {
|
|
|
434
434
|
process.stdout.write(`${JSON.stringify(toCursorHookOutput(eventName, result))}\n`);
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
const isMain =
|
|
437
|
+
const isMain =isMainModule(import.meta.url);
|
|
438
438
|
if (isMain) {
|
|
439
439
|
main().catch((err) => {
|
|
440
440
|
process.stderr.write(`cursor-hook-bridge: ${err instanceof Error ? err.message : err}\n`);
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
59
59
|
import { fileURLToPath } from 'node:url';
|
|
60
60
|
import { join, relative, resolve } from 'node:path';
|
|
61
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
61
62
|
|
|
62
63
|
/** Block-scalar indicator on the `description:` line: '>' (folded) or '|' (literal), optional chomping '+'/'-'. */
|
|
63
64
|
const BLOCK_SCALAR_RE = /^([>|])([+-]?)\s*$/;
|
|
@@ -526,10 +527,6 @@ function main() {
|
|
|
526
527
|
}
|
|
527
528
|
|
|
528
529
|
const __filename = fileURLToPath(import.meta.url);
|
|
529
|
-
const isMain =
|
|
530
|
-
typeof process !== 'undefined' &&
|
|
531
|
-
process.argv[1] !== null &&
|
|
532
|
-
process.argv[1] !== undefined &&
|
|
533
|
-
resolve(process.argv[1]) === resolve(__filename);
|
|
530
|
+
const isMain =isMainModule(import.meta.url);
|
|
534
531
|
|
|
535
532
|
if (isMain) main();
|
|
@@ -32,6 +32,7 @@ import { warn } from '../common.mjs';
|
|
|
32
32
|
import { acquire } from '../session-lock.mjs';
|
|
33
33
|
import { enumerateCandidates, freeCandidates } from './enumerate.mjs';
|
|
34
34
|
import { rankCandidates } from './rank.mjs';
|
|
35
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
35
36
|
|
|
36
37
|
const VERSION = '1.0.0';
|
|
37
38
|
|
|
@@ -247,7 +248,7 @@ export async function main(argv) {
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
// Run main only when executed directly, never on import.
|
|
250
|
-
if (import.meta.url
|
|
251
|
+
if (isMainModule(import.meta.url)) {
|
|
251
252
|
main(process.argv.slice(2))
|
|
252
253
|
.then((code) => process.exit(code))
|
|
253
254
|
.catch((err) => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { runEcosystemWizard } from './ecosystem-wizard/wizard-prompt.mjs';
|
|
10
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
10
11
|
|
|
11
12
|
export { detectCiProvider } from './ecosystem-wizard/ci-detector.mjs';
|
|
12
13
|
export {
|
|
@@ -31,7 +32,7 @@ export { runEcosystemWizard } from './ecosystem-wizard/wizard-prompt.mjs';
|
|
|
31
32
|
// CLI entry
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
33
34
|
|
|
34
|
-
if (
|
|
35
|
+
if (isMainModule(import.meta.url)) {
|
|
35
36
|
const args = process.argv.slice(2);
|
|
36
37
|
const repoRootIdx = args.indexOf('--repo-root');
|
|
37
38
|
const dryRun = args.includes('--dry-run');
|
|
@@ -30,6 +30,8 @@ import { promises as fs } from 'node:fs';
|
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import { execSync } from 'node:child_process';
|
|
32
32
|
|
|
33
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
34
|
+
|
|
33
35
|
// ---------------------------------------------------------------------------
|
|
34
36
|
// Configuration defaults (mirrors the .sh defaults)
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -296,14 +298,7 @@ async function _cliMain() {
|
|
|
296
298
|
process.exit(3);
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
|
|
300
|
-
const isMain = process.argv[1] && (
|
|
301
|
-
process.argv[1] === new URL(import.meta.url).pathname ||
|
|
302
|
-
// Handle invocation without file:// prefix
|
|
303
|
-
process.argv[1].endsWith('fetch-baseline.mjs')
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (isMain) {
|
|
301
|
+
if (isMainModule(import.meta.url)) {
|
|
307
302
|
_cliMain().catch((err) => {
|
|
308
303
|
process.stderr.write(`FATAL: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
309
304
|
process.exit(3);
|
|
@@ -24,6 +24,7 @@ import { promisify } from 'node:util';
|
|
|
24
24
|
|
|
25
25
|
import { detectVcsForRepo, discoverVaultRepos } from '../gitlab-portfolio/vcs-detect.mjs';
|
|
26
26
|
import { resolveRepoSpec } from '../vcs-repo-spec.mjs';
|
|
27
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
27
28
|
|
|
28
29
|
const realExecFile = promisify(execFileCb);
|
|
29
30
|
|
|
@@ -455,7 +456,7 @@ export async function main(argv, deps = {}) {
|
|
|
455
456
|
|
|
456
457
|
// ── CLI guard — prevent process.exit during test-time imports ─────────────────
|
|
457
458
|
|
|
458
|
-
if (import.meta.url
|
|
459
|
+
if (isMainModule(import.meta.url)) {
|
|
459
460
|
main(process.argv.slice(2))
|
|
460
461
|
.then((result) => {
|
|
461
462
|
process.exit(result.exitCode);
|
|
@@ -36,6 +36,7 @@ import { expandTilde } from '../common.mjs';
|
|
|
36
36
|
import { discoverVaultRepos } from './vcs-detect.mjs';
|
|
37
37
|
import { fetchIssuesMultiRepo, summarizeRepo } from './aggregator.mjs';
|
|
38
38
|
import { renderPortfolio, writePortfolio } from './markdown-writer.mjs';
|
|
39
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
39
40
|
|
|
40
41
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
42
|
|
|
@@ -406,7 +407,7 @@ EXIT CODES
|
|
|
406
407
|
|
|
407
408
|
// ── CLI guard — prevent process.exit during test-time imports ─────────────────
|
|
408
409
|
|
|
409
|
-
if (import.meta.url
|
|
410
|
+
if (isMainModule(import.meta.url)) {
|
|
410
411
|
main(process.argv.slice(2)).then((result) => {
|
|
411
412
|
process.exit(result.exitCode);
|
|
412
413
|
}).catch((err) => {
|