session-orchestrator 5.6.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -0
- package/.agents/skills/architecture/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.agents/skills/eval/SKILL.md +1 -1
- package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.agents/skills/hook-development/SKILL.md +1 -0
- package/.agents/skills/mcp-builder/SKILL.md +1 -0
- package/.agents/skills/tmux-layout/SKILL.md +3 -0
- package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +1 -0
- package/.agents/skills/vault-sync/SKILL.md +1 -0
- package/.agents/skills/write-executable-plan/SKILL.md +3 -0
- package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/architecture/SKILL.md +3 -0
- package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
- package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
- package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.cursor/commands/architecture.md +13 -0
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/tmux-layout.md +13 -0
- package/.cursor/commands/write-executable-plan.md +13 -0
- package/.cursor/skills/architecture/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/tmux-layout/SKILL.md +0 -1
- package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +75 -0
- package/README.md +10 -10
- package/agents/eval-judge.md +5 -5
- package/docs/USER-GUIDE.md +1 -1
- package/docs/baseline.md +4 -3
- package/docs/codex-setup.md +6 -4
- package/docs/components.md +5 -4
- package/docs/events-schema.md +4 -3
- package/docs/github-mirror-protection.md +3 -2
- package/docs/install.md +39 -3
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +12 -2
- package/docs/session-config-reference.md +8 -6
- package/docs/session-config-template.md +4 -3
- package/evals/README.md +60 -0
- package/evals/instruction-ablation/README.md +147 -0
- package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
- package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
- package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
- package/evals/instruction-ablation/run.mjs +336 -0
- package/evals/psa-003-foreign-work/case.yaml +16 -0
- package/evals/psa-003-foreign-work/fixture.sh +12 -0
- package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
- package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
- package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
- package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
- package/evals/psa-003-foreign-work/prompt.md +6 -0
- package/hooks/_lib/hook-import-set.json +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/post-tool-batch-wave-signal.mjs +1 -0
- package/hooks/post-tooluse-frontend-slop.mjs +11 -1
- package/hooks/pre-task-scope-disjoint.mjs +8 -6
- package/hooks/wave-scope-commit-guard.mjs +41 -10
- package/monitors/monitors.json +1 -1
- package/package.json +3 -1
- package/pi/prompts/architecture.md +11 -0
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/tmux-layout.md +11 -0
- package/pi/prompts/write-executable-plan.md +11 -0
- package/scripts/backfill-abandoned-sessions.mjs +6 -8
- package/scripts/backfill-learnings-from-vault.mjs +17 -6
- package/scripts/backfill-sessions.mjs +9 -7
- package/scripts/codex-install.mjs +108 -23
- package/scripts/emit-session.mjs +15 -2
- package/scripts/fleet-auflagen.mjs +54 -0
- package/scripts/generate-agents-skills.mjs +9 -16
- package/scripts/generate-codex-skills.mjs +8 -6
- package/scripts/generate-cursor-adapter.mjs +9 -74
- package/scripts/generate-pi-prompts.mjs +8 -74
- package/scripts/github-protection-audit.mjs +8 -0
- package/scripts/lib/auto-dialectic.mjs +10 -9
- package/scripts/lib/auto-dream.mjs +8 -10
- package/scripts/lib/autopilot/telemetry.mjs +12 -6
- package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
- package/scripts/lib/config/drift-check.mjs +1 -1
- package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/convergence-monitor.mjs +5 -3
- package/scripts/lib/dispatcher/rank.mjs +5 -1
- package/scripts/lib/ecosystem-health.mjs +5 -3
- package/scripts/lib/eval/engine.mjs +61 -27
- package/scripts/lib/eval/judge.mjs +14 -8
- package/scripts/lib/eval/session-resolve.mjs +7 -2
- package/scripts/lib/events-retention-banner.mjs +5 -2
- package/scripts/lib/events.mjs +81 -54
- package/scripts/lib/fleet-protocol.mjs +98 -1
- package/scripts/lib/instruction-budget-guard.mjs +25 -18
- package/scripts/lib/io.mjs +181 -39
- package/scripts/lib/learnings/candidates.mjs +14 -44
- package/scripts/lib/learnings/io.mjs +7 -2
- package/scripts/lib/learnings/judgment.mjs +5 -8
- package/scripts/lib/learnings/select.mjs +3 -3
- package/scripts/lib/maintenance-due-banner.mjs +2 -2
- package/scripts/lib/memory-proposals/sink.mjs +5 -6
- package/scripts/lib/memory-proposals/store.mjs +12 -7
- package/scripts/lib/mirror-issues-banner.mjs +11 -1
- package/scripts/lib/native-command.mjs +48 -0
- package/scripts/lib/orphan-reaper/scan.mjs +29 -3
- package/scripts/lib/peer-cards/writer.mjs +6 -18
- package/scripts/lib/platform.mjs +113 -4
- package/scripts/lib/plugin-package-stage.mjs +169 -0
- package/scripts/lib/plugin-surface.mjs +121 -0
- package/scripts/lib/quality-gate.mjs +30 -25
- package/scripts/lib/reconcile/idempotency.mjs +9 -10
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +97 -8
- package/scripts/lib/reconcile/writer.mjs +11 -17
- package/scripts/lib/rule-loader.mjs +32 -6
- package/scripts/lib/scope-echo.mjs +12 -2
- package/scripts/lib/scope-gate.mjs +2 -1
- package/scripts/lib/session-close-backfill.mjs +21 -4
- package/scripts/lib/session-id.mjs +2 -1
- package/scripts/lib/session-record-repair.mjs +9 -13
- package/scripts/lib/session-schema/constants.mjs +20 -5
- package/scripts/lib/session-start-probes.mjs +2 -2
- package/scripts/lib/sessions-staleness-banner.mjs +2 -3
- package/scripts/lib/skill-evolution/engine.mjs +6 -9
- package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
- package/scripts/lib/slopcheck.mjs +9 -7
- package/scripts/lib/sunset/walker.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +3 -1
- package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
- package/scripts/lib/test-sibling-coverage.mjs +167 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
- package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
- package/scripts/lib/user-invocable-skills.mjs +37 -10
- package/scripts/lib/ux-grill/run-record.mjs +5 -3
- package/scripts/lib/validate/check-banner-parity.mjs +1 -0
- package/scripts/lib/validate/check-skills.mjs +12 -2
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
- package/scripts/lib/validate/check-test-value-bans.mjs +12 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/vault-archive.mjs +2 -4
- package/scripts/lib/vault-mirror/process.mjs +585 -72
- package/scripts/lib/vault-mirror/prune.mjs +413 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +13 -5
- package/scripts/lib/vault-mirror/retention.mjs +388 -0
- package/scripts/lib/vault-mirror/utils.mjs +16 -1
- package/scripts/lib/vault-status/board-writer.mjs +6 -6
- package/scripts/lib/vault-sync-baseline.mjs +9 -8
- package/scripts/lib/wave-transcript-tail.mjs +19 -6
- package/scripts/lib/worktree/meta.mjs +7 -3
- package/scripts/mcp-server.mjs +158 -0
- package/scripts/migrate-sessions-jsonl.mjs +7 -5
- package/scripts/migrate-vault-paths.mjs +7 -4
- package/scripts/print-applicable-rules.mjs +1 -25
- package/scripts/print-learnings-index.mjs +2 -1
- package/scripts/release.mjs +4 -0
- package/scripts/relocate-vault-corpus.mjs +5 -3
- package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
- package/scripts/self-update.mjs +179 -39
- package/scripts/site-numbers.mjs +4 -3
- package/scripts/sweep-expired-learnings.mjs +6 -4
- package/scripts/sync-vault-schema.mjs +3 -7
- package/scripts/vault-mirror-prune.mjs +145 -0
- package/scripts/vault-mirror.mjs +77 -12
- package/scripts/windows-setup.mjs +45 -0
- package/skills/_shared/fleet-protocol.md +8 -0
- package/skills/architecture/SKILL.md +1 -0
- package/skills/bootstrap/_shared-template.md +5 -2
- package/skills/claude-md-drift-check/SKILL.md +4 -1
- package/skills/claude-md-drift-check/checker.mjs +89 -26
- package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
- package/skills/eval/SKILL.md +12 -13
- package/skills/eval/rubric-v3.md +521 -0
- package/skills/frontmatter-guard/SKILL.md +1 -0
- package/skills/gitlab-portfolio/SKILL.md +1 -0
- package/skills/hook-development/SKILL.md +1 -0
- package/skills/mcp-builder/SKILL.md +1 -0
- package/skills/repo-audit/SKILL.md +2 -2
- package/skills/session-end/SKILL.md +32 -1
- package/skills/session-end/vault-operations.md +12 -2
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/phase-7-1-premise-check.md +2 -1
- package/skills/session-start/references/operations-contract.md +4 -1
- package/skills/tmux-layout/SKILL.md +1 -0
- package/skills/vault-mirror/SKILL.md +28 -2
- package/skills/vault-sync/SKILL.md +6 -6
- package/skills/vault-sync/validator.sh +29 -10
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +5 -3
- package/skills/write-executable-plan/SKILL.md +1 -0
- package/hooks/agent-teams-h3-test.sh +0 -362
- package/scripts/spikes/h3-agent-teams/preflight.sh +0 -53
- package/scripts/spikes/h3-agent-teams/run-h3.sh +0 -112
- package/scripts/spikes/h3-agent-teams/setup.sh +0 -137
- package/scripts/spikes/h3-agent-teams/toggle.sh +0 -38
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Native, dependency-free MCP stdio transport. No shell, WSL, or jq required. */
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { isSessionConfigHeading } from './lib/config/section-extractor.mjs';
|
|
8
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
9
|
+
|
|
10
|
+
const SERVER_INFO = { name: 'session-orchestrator', version: '2.0.0' };
|
|
11
|
+
const TOOLS = [
|
|
12
|
+
{
|
|
13
|
+
name: 'session_config',
|
|
14
|
+
description: 'Reads Session Config section from the project instruction file (CLAUDE.md or AGENTS.md alias)',
|
|
15
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'session_metrics',
|
|
19
|
+
description: 'Reads the last 5 REAL session metrics entries from .orchestrator/metrics/sessions.jsonl (abandoned phantom stubs excluded)',
|
|
20
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const textContent = (text) => ({ content: [{ type: 'text', text }] });
|
|
24
|
+
const errorResponse = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
25
|
+
|
|
26
|
+
/** Resolve the caller's repository without passing native paths through a shell. */
|
|
27
|
+
export function resolveProjectRoot(cwd = process.cwd()) {
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
30
|
+
cwd, encoding: 'utf8', windowsHide: true, timeout: 10_000,
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
}).trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isFile(path, nonempty = false) {
|
|
39
|
+
try {
|
|
40
|
+
const stat = statSync(path);
|
|
41
|
+
return stat.isFile() && (!nonempty || stat.size > 0);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read the same raw config body as the legacy shell tool (including comments). */
|
|
49
|
+
export function readSessionConfig(root) {
|
|
50
|
+
const file = ['CLAUDE.md', 'AGENTS.md'].map((name) => join(root, name))
|
|
51
|
+
.find((path) => isFile(path, true));
|
|
52
|
+
if (!file) return `No CLAUDE.md or AGENTS.md found at ${root}`;
|
|
53
|
+
const body = [];
|
|
54
|
+
let inSection = false;
|
|
55
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
56
|
+
if (isSessionConfigHeading(line)) {
|
|
57
|
+
inSection = true;
|
|
58
|
+
} else if (line.startsWith('## ')) {
|
|
59
|
+
inSection = false;
|
|
60
|
+
} else if (inSection) {
|
|
61
|
+
body.push(line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Shell command substitution removes LF bytes, not spaces or CR bytes.
|
|
65
|
+
return body.join('\n').replace(/\n+$/, '') || `No '## Session Config' section found in ${file}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read the last five real records, with whole-ledger stub count and token coverage. */
|
|
69
|
+
export function readSessionMetrics(root) {
|
|
70
|
+
const file = ['.orchestrator', '.claude'].map((dir) => join(root, dir, 'metrics', 'sessions.jsonl'))
|
|
71
|
+
.find((path) => isFile(path));
|
|
72
|
+
if (!file) return 'No metrics found (checked .orchestrator/metrics/ and .claude/metrics/)';
|
|
73
|
+
const entries = [];
|
|
74
|
+
let stubCount = 0;
|
|
75
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
76
|
+
let record;
|
|
77
|
+
try { record = JSON.parse(line); } catch { continue; }
|
|
78
|
+
// Torn writes and non-record JSON must not hide subsequent valid sessions.
|
|
79
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) continue;
|
|
80
|
+
if (record.status === 'abandoned') { stubCount++; continue; }
|
|
81
|
+
entries.push(record);
|
|
82
|
+
if (entries.length > 5) entries.shift();
|
|
83
|
+
}
|
|
84
|
+
if (!entries.length) {
|
|
85
|
+
return stubCount ? `No real sessions (abandoned stubs excluded: ${stubCount})`
|
|
86
|
+
: 'No metrics found (file is empty)';
|
|
87
|
+
}
|
|
88
|
+
const record = entries.findLast((entry) => (entry.total_token_input !== null && entry.total_token_input !== undefined)
|
|
89
|
+
|| (entry.total_token_output !== null && entry.total_token_output !== undefined));
|
|
90
|
+
const coverage = record?.subagents_with_tokens ?? 0;
|
|
91
|
+
const summary = record
|
|
92
|
+
? `tokens: ${record.total_token_input ?? '?'} in / ${record.total_token_output ?? '?'} out`
|
|
93
|
+
+ ` (coverage: ${coverage} subagents)`
|
|
94
|
+
+ (coverage === 0 ? ' ⚠ partial — subagent token data missing; total is not a reliable cost estimate' : '')
|
|
95
|
+
+ ` [session: ${record.session_id ?? 'unknown'}]`
|
|
96
|
+
: 'tokens: no token data in last 5 sessions (subagent telemetry not yet captured)';
|
|
97
|
+
return entries.map((entry) => JSON.stringify(entry)).join('\n')
|
|
98
|
+
+ `\n\nabandoned stubs excluded: ${stubCount}\n\n--- token summary ---\n${summary}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Handle one JSON-RPC line. null means a notification or blank line, not a reply. */
|
|
102
|
+
export function handleLine(line, { cwd = process.cwd(), resolveRoot = resolveProjectRoot } = {}) {
|
|
103
|
+
if (!line.trim()) return null;
|
|
104
|
+
let request;
|
|
105
|
+
try { request = JSON.parse(line); } catch { return errorResponse(null, -32700, 'Parse error'); }
|
|
106
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)) {
|
|
107
|
+
return errorResponse(null, -32600, 'Invalid Request');
|
|
108
|
+
}
|
|
109
|
+
const hasId = Object.hasOwn(request, 'id');
|
|
110
|
+
const validId = request.id === null || typeof request.id === 'string' || typeof request.id === 'number';
|
|
111
|
+
const id = hasId && validId ? request.id : null;
|
|
112
|
+
if (request.jsonrpc !== '2.0' || typeof request.method !== 'string' || (hasId && !validId)) {
|
|
113
|
+
return errorResponse(id, -32600, 'Invalid Request');
|
|
114
|
+
}
|
|
115
|
+
if (!hasId || request.method.startsWith('notifications/')) return null;
|
|
116
|
+
let result;
|
|
117
|
+
switch (request.method) {
|
|
118
|
+
case 'initialize':
|
|
119
|
+
result = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: SERVER_INFO };
|
|
120
|
+
break;
|
|
121
|
+
case 'tools/list':
|
|
122
|
+
result = { tools: TOOLS };
|
|
123
|
+
break;
|
|
124
|
+
case 'tools/call': {
|
|
125
|
+
const name = request.params?.name;
|
|
126
|
+
if (typeof name !== 'string') return errorResponse(id, -32602, 'Invalid tool name');
|
|
127
|
+
if (!TOOLS.some((tool) => tool.name === name)) return errorResponse(id, -32602, `Unknown tool: ${name}`);
|
|
128
|
+
try {
|
|
129
|
+
const root = resolveRoot(cwd);
|
|
130
|
+
result = textContent(root
|
|
131
|
+
? (name === 'session_config' ? readSessionConfig(root) : readSessionMetrics(root))
|
|
132
|
+
: 'Error: not inside a git repository');
|
|
133
|
+
} catch {
|
|
134
|
+
return errorResponse(id, -32603, 'Unable to read session data');
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
return errorResponse(id, -32601, `Method not found: ${request.method}`);
|
|
140
|
+
}
|
|
141
|
+
return { jsonrpc: '2.0', id, result };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Start newline-delimited JSON-RPC on the supplied streams. */
|
|
145
|
+
export async function serve({ input = process.stdin, output = process.stdout, cwd = process.cwd() } = {}) {
|
|
146
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
147
|
+
for await (const line of lines) {
|
|
148
|
+
const response = handleLine(line, { cwd });
|
|
149
|
+
if (response) output.write(`${JSON.stringify(response)}\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isMainModule(import.meta.url)) {
|
|
154
|
+
serve().catch(() => {
|
|
155
|
+
process.stderr.write('session-orchestrator: MCP stdio transport failed.\n');
|
|
156
|
+
process.exitCode = 2;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
* "already_canonical":N,"unmappable":N,"parse_errors":N,"backup":"<path>|null"}
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { readFileSync,
|
|
42
|
-
import {
|
|
41
|
+
import { readFileSync, copyFileSync, existsSync } from 'node:fs';
|
|
42
|
+
import { basename } from 'node:path';
|
|
43
|
+
import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
|
|
43
44
|
import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
|
|
44
45
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
45
46
|
|
|
@@ -422,11 +423,12 @@ async function main() {
|
|
|
422
423
|
// Atomic write: backup → tmp → rename
|
|
423
424
|
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
424
425
|
const backupPath = `${args.file}.bak-${iso}`;
|
|
425
|
-
const tmpPath = join(dirname(args.file), `.${basename(args.file)}.migrate-tmp-${process.pid}`);
|
|
426
426
|
try {
|
|
427
427
|
copyFileSync(args.file, backupPath);
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
const res = atomicWriteWithBackup(args.file, outputLines.join('\n') + '\n', {
|
|
429
|
+
tmpPrefix: `.${basename(args.file)}.tmp`,
|
|
430
|
+
});
|
|
431
|
+
if (!res.ok) throw envelopeToError(res);
|
|
430
432
|
summary.backup = backupPath;
|
|
431
433
|
} catch (err) {
|
|
432
434
|
const hint = existsSync(backupPath)
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from './lib/vault-migration-rules.mjs';
|
|
59
59
|
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
60
60
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
61
|
+
import { atomicWriteText, envelopeToError } from './lib/io.mjs';
|
|
61
62
|
|
|
62
63
|
// ---------------------------------------------------------------------------
|
|
63
64
|
// Constants
|
|
@@ -529,12 +530,14 @@ function findHits(filePath, content) {
|
|
|
529
530
|
}
|
|
530
531
|
|
|
531
532
|
/**
|
|
532
|
-
* Atomic write
|
|
533
|
+
* Atomic write via `atomicWriteText` (`<dir>/.<name>.tmp.<hex>`, then rename);
|
|
534
|
+
* throws with the original message and `code` on failure.
|
|
533
535
|
*/
|
|
534
536
|
async function atomicWrite(filePath, content) {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
537
|
+
const res = await atomicWriteText(filePath, content, {
|
|
538
|
+
tmpPrefix: `.${path.basename(filePath)}.tmp`,
|
|
539
|
+
});
|
|
540
|
+
if (!res.ok) throw envelopeToError(res);
|
|
538
541
|
}
|
|
539
542
|
|
|
540
543
|
/**
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
import { parseArgs } from 'node:util';
|
|
99
99
|
import { readFileSync } from 'node:fs';
|
|
100
100
|
import { join, relative } from 'node:path';
|
|
101
|
-
import { createHash } from 'node:crypto';
|
|
102
101
|
|
|
103
102
|
import { findProjectRoot } from './lib/common.mjs';
|
|
103
|
+
import { deriveFenceToken } from './lib/reconcile/sanitize.mjs';
|
|
104
104
|
import { loadApplicableRules } from './lib/rule-loader.mjs';
|
|
105
105
|
import { readHostClass } from './lib/autopilot/telemetry.mjs';
|
|
106
106
|
import { resolveStateArtifactPath, resolveStateMdPath } from './lib/state-md.mjs';
|
|
@@ -218,30 +218,6 @@ function neutraliseWrapperForgeries(text) {
|
|
|
218
218
|
return out;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
/**
|
|
222
|
-
* Derive this block's fence token from its own payload.
|
|
223
|
-
*
|
|
224
|
-
* Deterministic by construction (same input → same token), so the CLI's stdout
|
|
225
|
-
* stays reproducible. The re-derivation loop makes the absence guarantee
|
|
226
|
-
* STRUCTURAL rather than probabilistic: a token that literally occurred in the
|
|
227
|
-
* payload would be forgeable in a closing tag, so we re-hash with a counter
|
|
228
|
-
* until it does not occur. Each iteration is a fresh 32-bit draw against a
|
|
229
|
-
* fixed payload, so termination is immediate in practice; the cap exists only
|
|
230
|
-
* so a pathological input cannot spin, and its fallback (the full 64-hex
|
|
231
|
-
* digest, which no realistic rule body contains) still satisfies the guarantee.
|
|
232
|
-
*
|
|
233
|
-
* @param {string} payload - the concatenated rule bodies this token must fence
|
|
234
|
-
* @returns {string} a hex token provably absent from `payload`
|
|
235
|
-
*/
|
|
236
|
-
function deriveFenceToken(payload) {
|
|
237
|
-
const digest = (salt) => createHash('sha256').update(`${salt}\n${payload}`).digest('hex');
|
|
238
|
-
for (let salt = 0; salt < 64; salt++) {
|
|
239
|
-
const token = digest(salt).slice(0, 8);
|
|
240
|
-
if (!payload.includes(token)) return token;
|
|
241
|
-
}
|
|
242
|
-
return digest(64);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
221
|
/**
|
|
246
222
|
* Render a rule's `src` attribute: repo-relative (an absolute path would print
|
|
247
223
|
* the operator's home directory into every agent prompt) and reduced to a
|
|
@@ -101,6 +101,7 @@ import { join, dirname } from 'node:path';
|
|
|
101
101
|
import { fileURLToPath } from 'node:url';
|
|
102
102
|
|
|
103
103
|
import { findProjectRoot } from './lib/common.mjs';
|
|
104
|
+
import { eventsFilePath } from './lib/events.mjs';
|
|
104
105
|
import { resolveStateArtifactPath } from './lib/state-md.mjs';
|
|
105
106
|
import {
|
|
106
107
|
CANDIDATE_POOL_SIZE,
|
|
@@ -249,7 +250,7 @@ const repoRoot = findProjectRoot(process.cwd());
|
|
|
249
250
|
const learningsPath = opts.learnings
|
|
250
251
|
? opts.learnings
|
|
251
252
|
: join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
|
|
252
|
-
const eventsPath =
|
|
253
|
+
const eventsPath = eventsFilePath(repoRoot); // #1514 point 1: the repo's one ledger
|
|
253
254
|
// #1019 — the natively-delivered rule corpus. Every `.claude/rules/*.md` reaches
|
|
254
255
|
// a dispatched agent in FULL through Claude Code's own project-instruction
|
|
255
256
|
// loading (`docs/instruction-delivery.md` §1: the `globs:`/`tier:` frontmatter is
|
package/scripts/release.mjs
CHANGED
|
@@ -387,6 +387,10 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
|
|
|
387
387
|
// `scripts/lib/locks/index.mjs` is the one-cycle deprecation shim (5.2.0 CHANGELOG, Changed): its
|
|
388
388
|
// runtime warning names the version it was deprecated IN, so it legitimately carries the previous
|
|
389
389
|
// release literal until its 6.0.0 removal — version history by construction, not drift.
|
|
390
|
+
// `site/_census.json` is deliberately NOT allowlisted: it carries a `"version"` field that
|
|
391
|
+
// `--set-version` re-stamps (scripts/site-numbers.mjs --write, see the post-bump hint in main()),
|
|
392
|
+
// so leaving it under the drift sweep makes the sweep double as its staleness gate — a census
|
|
393
|
+
// not re-stamped after a bump fails `--check` instead of shipping the previous version.
|
|
390
394
|
export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|scripts\/lib\/locks\/index\.mjs|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
|
|
391
395
|
|
|
392
396
|
/** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import { buildBackfillIndex, parseSessionId } from './lib/vault-repo-backfill.mjs';
|
|
56
56
|
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
57
57
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
58
|
+
import { atomicWriteText, envelopeToError } from './lib/io.mjs';
|
|
58
59
|
|
|
59
60
|
// ---------------------------------------------------------------------------
|
|
60
61
|
// Constants
|
|
@@ -693,7 +694,6 @@ async function writeManifest({ vaultDir, derivableOnly, summary, moves }) {
|
|
|
693
694
|
|
|
694
695
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
695
696
|
const manifestPath = path.join(manifestDir, `relocation-manifest-${ts}.json`);
|
|
696
|
-
const tmpPath = `${manifestPath}.tmp-${process.pid}`;
|
|
697
697
|
|
|
698
698
|
// Store vault-relative paths in the manifest
|
|
699
699
|
const movesRel = moves.map((m) => ({
|
|
@@ -714,8 +714,10 @@ async function writeManifest({ vaultDir, derivableOnly, summary, moves }) {
|
|
|
714
714
|
moves: movesRel,
|
|
715
715
|
};
|
|
716
716
|
|
|
717
|
-
await
|
|
718
|
-
|
|
717
|
+
const res = await atomicWriteText(manifestPath, JSON.stringify(manifest, null, 2) + '\n', {
|
|
718
|
+
tmpPrefix: `.${path.basename(manifestPath)}.tmp`,
|
|
719
|
+
});
|
|
720
|
+
if (!res.ok) throw envelopeToError(res);
|
|
719
721
|
|
|
720
722
|
return manifestPath;
|
|
721
723
|
}
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
* with the next repo, and exits 2 at the end
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { existsSync, readFileSync, writeFileSync, copyFileSync
|
|
35
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
36
36
|
import { mkdirSync } from 'node:fs';
|
|
37
|
-
import { dirname, join } from 'node:path';
|
|
37
|
+
import { basename, dirname, join } from 'node:path';
|
|
38
38
|
import { homedir } from 'node:os';
|
|
39
39
|
import {
|
|
40
40
|
migrateLegacyLearning,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import { LearningsLockError, withLearningsLock } from './lib/learnings/io.mjs';
|
|
44
44
|
import { getCrossRepoProjects, getConfinementRoot } from './lib/config/cross-repo.mjs';
|
|
45
45
|
import { validatePathInsideProject } from './lib/path-utils.mjs';
|
|
46
|
+
import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
|
|
46
47
|
|
|
47
48
|
const LEARNINGS_REL = '.orchestrator/metrics/learnings.jsonl';
|
|
48
49
|
|
|
@@ -316,12 +317,13 @@ function migrateStore(repoPath, learningsPath, apply) {
|
|
|
316
317
|
try {
|
|
317
318
|
// Backup original
|
|
318
319
|
copyFileSync(learningsPath, backupPath);
|
|
319
|
-
// Write migrated content atomically
|
|
320
|
+
// Write migrated content atomically (own backup above keeps its
|
|
321
|
+
// `.bak-cross-repo-migrate-<ms>` schema, so the helper takes none)
|
|
320
322
|
const body = outputLines.join('\n') + '\n';
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
const res = atomicWriteWithBackup(learningsPath, body, {
|
|
324
|
+
tmpPrefix: `.${basename(learningsPath)}.tmp`,
|
|
325
|
+
});
|
|
326
|
+
if (!res.ok) throw envelopeToError(res);
|
|
325
327
|
} catch (err) {
|
|
326
328
|
return {
|
|
327
329
|
repo: repoPath,
|
package/scripts/self-update.mjs
CHANGED
|
@@ -15,10 +15,19 @@
|
|
|
15
15
|
* Steps (each skipped when its harness is not installed on this host):
|
|
16
16
|
* 1. repo — `git fetch origin` + `git merge --ff-only origin/main`, then
|
|
17
17
|
* `npm ci` when HEAD moved or node_modules is missing.
|
|
18
|
-
* 2. claude —
|
|
19
|
-
*
|
|
18
|
+
* 2. claude — `npm pack` this clone and extract it into STAGE_DIR, then
|
|
19
|
+
* marketplace update + plugin update, then verify version and
|
|
20
|
+
* runtime dependencies in the reported installPath. The
|
|
21
|
+
* marketplace must read STAGE_DIR, not this clone: a directory
|
|
22
|
+
* marketplace on the working checkout copied ALL of it into the
|
|
23
|
+
* plugin cache — 7557 files instead of 1164, `.env.local`
|
|
24
|
+
* (NPM_TOKEN) and `.orchestrator/` included (#1515, measured
|
|
25
|
+
* 2026-10-03). The step refuses, naming the remove + add
|
|
26
|
+
* commands, while ANY directory marketplace reads this clone;
|
|
27
|
+
* GitHub/npm sources update as before, with a one-line hint.
|
|
20
28
|
* 3. codex — only if the plugin is already installed in Codex: re-run
|
|
21
|
-
* scripts/codex-install.mjs
|
|
29
|
+
* scripts/codex-install.mjs, which installs from the same
|
|
30
|
+
* packed STAGE_DIR (#1518), never from this clone.
|
|
22
31
|
*
|
|
23
32
|
* Never deletes an old plugin cache directory: the running session still
|
|
24
33
|
* loads it until the harness restarts.
|
|
@@ -26,20 +35,78 @@
|
|
|
26
35
|
* Exit codes: 0 all steps ok/skipped · 1 usage error · 2 a step failed.
|
|
27
36
|
*/
|
|
28
37
|
|
|
29
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
38
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
30
40
|
import path from 'node:path';
|
|
31
|
-
import {
|
|
41
|
+
import { commandOnPath, spawnNativeSync } from './lib/native-command.mjs';
|
|
32
42
|
import { fileURLToPath } from 'node:url';
|
|
33
43
|
|
|
34
44
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
45
|
+
import { resolveStageDir, stagePackage } from './lib/plugin-package-stage.mjs';
|
|
35
46
|
|
|
36
47
|
const SO_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
48
|
+
// Codex install id; the Claude id is read from the installed entry, because
|
|
49
|
+
// the marketplace name is whatever the host registered (`kanevry` per
|
|
50
|
+
// .claude-plugin/marketplace.json, `session-orchestrator` on older hosts).
|
|
37
51
|
const PLUGIN_ID = 'session-orchestrator@kanevry';
|
|
38
|
-
const
|
|
52
|
+
const PLUGIN_NAME = 'session-orchestrator';
|
|
39
53
|
const COMMAND_TIMEOUT_MS = 300_000;
|
|
40
54
|
// `codex plugin list --available --json` is 2.56 MB on a host with several
|
|
41
55
|
// marketplaces (measured 2026-10-02); the 1 MiB default fails with ENOBUFS.
|
|
42
56
|
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
57
|
+
/** Host-local copy of the packed package; the marketplace reads this, never the clone. */
|
|
58
|
+
const STAGE_DIR = resolveStageDir();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Claude Code config dir: CLAUDE_CONFIG_DIR when absolute, else ~/.claude. A
|
|
62
|
+
* relative value is ignored like a relative XDG_CACHE_HOME — resolved against
|
|
63
|
+
* whatever cwd this script runs in, it would point the marketplace guard at a
|
|
64
|
+
* file that does not exist and let the update run unguarded (#1519).
|
|
65
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
66
|
+
* @param {string} [home]
|
|
67
|
+
* @returns {{dir: string, ignored: string | null}}
|
|
68
|
+
*/
|
|
69
|
+
export function resolveClaudeConfigDir(env = process.env, home = os.homedir()) {
|
|
70
|
+
const raw = (env.CLAUDE_CONFIG_DIR || '').trim();
|
|
71
|
+
if (path.isAbsolute(raw)) return { dir: raw, ignored: null };
|
|
72
|
+
return { dir: path.join(home, '.claude'), ignored: raw || null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CLAUDE_CONFIG = resolveClaudeConfigDir();
|
|
76
|
+
const KNOWN_MARKETPLACES = path.join(CLAUDE_CONFIG.dir, 'plugins', 'known_marketplaces.json');
|
|
77
|
+
|
|
78
|
+
const realOrResolved = (p) => { try { return realpathSync(p); } catch { return path.resolve(p); } };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Registered Claude Code marketplaces from `known_marketplaces.json`, read
|
|
82
|
+
* defensively: a missing or unparseable file yields `null`, never a throw.
|
|
83
|
+
* @param {string} file
|
|
84
|
+
* @returns {Array<{name: string, source: string | undefined, path: string | undefined}> | null}
|
|
85
|
+
*/
|
|
86
|
+
export function readKnownMarketplaces(file) {
|
|
87
|
+
let raw;
|
|
88
|
+
try { raw = JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
89
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
90
|
+
return Object.entries(raw).map(([name, v]) => ({
|
|
91
|
+
name,
|
|
92
|
+
source: typeof v?.source?.source === 'string' ? v.source.source : undefined,
|
|
93
|
+
path: typeof v?.source?.path === 'string' ? v.source.path : undefined,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Names of the `directory` marketplaces that read `dir` (realpath-compared) —
|
|
99
|
+
* whatever they are called.
|
|
100
|
+
* @param {ReturnType<typeof readKnownMarketplaces>} known
|
|
101
|
+
* @param {string} dir
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
export function directoryMarketplacesOn(known, dir) {
|
|
105
|
+
const target = realOrResolved(dir);
|
|
106
|
+
return (known ?? [])
|
|
107
|
+
.filter((m) => m.source === 'directory' && m.path && realOrResolved(m.path) === target)
|
|
108
|
+
.map((m) => m.name);
|
|
109
|
+
}
|
|
43
110
|
|
|
44
111
|
/**
|
|
45
112
|
* Pick the session-orchestrator entry from `claude plugin list --json`.
|
|
@@ -50,8 +117,9 @@ const MAX_BUFFER = 64 * 1024 * 1024;
|
|
|
50
117
|
export function findClaudeEntry(listJson) {
|
|
51
118
|
const entries = Array.isArray(listJson) ? listJson : listJson?.plugins;
|
|
52
119
|
if (!Array.isArray(entries)) return null;
|
|
53
|
-
|
|
54
|
-
|
|
120
|
+
const ours = (e) => typeof e?.id === 'string' && e.id.startsWith(`${PLUGIN_NAME}@`);
|
|
121
|
+
return entries.find((e) => ours(e) && e?.scope === 'user')
|
|
122
|
+
?? entries.find(ours)
|
|
55
123
|
?? null;
|
|
56
124
|
}
|
|
57
125
|
|
|
@@ -90,16 +158,16 @@ to the newest Session Orchestrator, and verify each one afterwards.
|
|
|
90
158
|
--json Print the per-step result as JSON
|
|
91
159
|
`;
|
|
92
160
|
|
|
93
|
-
function makeRunner({ dryRun, json }) {
|
|
161
|
+
export function makeRunner({ dryRun, json }) {
|
|
94
162
|
const log = (line) => { if (!json) process.stdout.write(`${line}\n`); };
|
|
95
163
|
/** Run a command; read-only queries also run under --dry-run. */
|
|
96
|
-
function run(cmd, args, { cwd = SO_ROOT, mutates = true } = {}) {
|
|
164
|
+
function run(cmd, args, { cwd = SO_ROOT, mutates = true, env } = {}) {
|
|
97
165
|
const shown = `${cmd} ${args.join(' ')}`;
|
|
98
166
|
if (dryRun && mutates) {
|
|
99
167
|
log(` [dry-run] ${shown}${cwd === SO_ROOT ? '' : ` (in ${cwd})`}`);
|
|
100
168
|
return { ok: true, stdout: '' };
|
|
101
169
|
}
|
|
102
|
-
const r =
|
|
170
|
+
const r = spawnNativeSync(cmd, args, { cwd, env, encoding: 'utf8', timeout: COMMAND_TIMEOUT_MS, maxBuffer: MAX_BUFFER });
|
|
103
171
|
if (r.error) return { ok: false, detail: `${shown}: ${r.error.message}` };
|
|
104
172
|
if (r.status !== 0) {
|
|
105
173
|
return { ok: false, detail: `${shown} exited ${r.status}: ${(r.stderr || r.stdout || '').trim().slice(-400)}` };
|
|
@@ -109,12 +177,8 @@ function makeRunner({ dryRun, json }) {
|
|
|
109
177
|
return { run, log };
|
|
110
178
|
}
|
|
111
179
|
|
|
112
|
-
function
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function repoVersion() {
|
|
117
|
-
return JSON.parse(readFileSync(path.join(SO_ROOT, 'package.json'), 'utf8')).version;
|
|
180
|
+
function repoVersion(root = SO_ROOT) {
|
|
181
|
+
return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
118
182
|
}
|
|
119
183
|
|
|
120
184
|
function updateRepo({ run, log }, opts) {
|
|
@@ -139,36 +203,112 @@ function updateRepo({ run, log }, opts) {
|
|
|
139
203
|
return { step: 'repo', status: 'ok', detail: `${repoVersion()}${moved ? ' (updated)' : ' (already current)'}` };
|
|
140
204
|
}
|
|
141
205
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
/** Marketplace name the packed copy registers under (its own marketplace.json). */
|
|
207
|
+
function packedMarketplaceName(root) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(readFileSync(path.join(root, '.claude-plugin', 'marketplace.json'), 'utf8')).name || 'kanevry';
|
|
210
|
+
} catch {
|
|
211
|
+
return 'kanevry';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The Claude Code step. `ctx` exists for tests (tmp config dir, tmp clone,
|
|
217
|
+
* no PATH lookup); production passes nothing.
|
|
218
|
+
*/
|
|
219
|
+
export function updateClaude({ run, log }, opts, ctx = {}) {
|
|
220
|
+
const {
|
|
221
|
+
soRoot = SO_ROOT,
|
|
222
|
+
stageDir = STAGE_DIR,
|
|
223
|
+
knownMarketplacesFile = KNOWN_MARKETPLACES,
|
|
224
|
+
ignoredConfigDir = CLAUDE_CONFIG.ignored,
|
|
225
|
+
hasClaude = () => commandOnPath('claude'),
|
|
226
|
+
} = ctx;
|
|
227
|
+
const fail = (detail) => ({ step: 'claude', status: 'failed', detail });
|
|
228
|
+
if (!hasClaude()) return { step: 'claude', status: 'skipped', detail: 'claude not on PATH' };
|
|
229
|
+
if (ignoredConfigDir) {
|
|
230
|
+
log(`claude: ignoring relative CLAUDE_CONFIG_DIR '${ignoredConfigDir}' — reading ${knownMarketplacesFile}`);
|
|
231
|
+
}
|
|
232
|
+
const stage = () => stagePackage({
|
|
233
|
+
soRoot, stageDir, run, log: (line) => log(`claude: ${line}`), dryRun: opts.dryRun,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const parseEntry = (r) => {
|
|
237
|
+
if (!r.ok) return { error: r.detail };
|
|
238
|
+
try { return { entry: findClaudeEntry(JSON.parse(r.stdout)) }; } catch (e) {
|
|
239
|
+
return { error: `plugin list --json unparseable: ${e.message}` };
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
// Installed-check first: with nothing installed there is nothing to guard,
|
|
243
|
+
// and the refusal below would replace the host's stage dir for no install (#1519).
|
|
244
|
+
const before = parseEntry(run('claude', ['plugin', 'list', '--json'], { mutates: false }));
|
|
245
|
+
if (before.error) return fail(before.error);
|
|
246
|
+
if (!before.entry) return { step: 'claude', status: 'skipped', detail: `${PLUGIN_NAME} not installed` };
|
|
247
|
+
|
|
248
|
+
const known = readKnownMarketplaces(knownMarketplacesFile);
|
|
249
|
+
const leaking = directoryMarketplacesOn(known, soRoot);
|
|
250
|
+
if (leaking.length > 0) {
|
|
251
|
+
// Stage first so the `add` target named below exists.
|
|
252
|
+
const staged = stage();
|
|
253
|
+
const packedName = packedMarketplaceName(soRoot);
|
|
254
|
+
// A marketplace already holding the packed copy's name (e.g. a GitHub
|
|
255
|
+
// `kanevry`) would make `marketplace add <stage>` fail on the taken name.
|
|
256
|
+
const sameName = known?.find((m) => m.name === packedName);
|
|
257
|
+
const nameTaken = sameName && !leaking.includes(packedName)
|
|
258
|
+
&& !(sameName.source === 'directory' && sameName.path && realOrResolved(sameName.path) === realOrResolved(stageDir));
|
|
259
|
+
const removes = [...leaking, ...(nameTaken ? [packedName] : [])]
|
|
260
|
+
.map((n) => `claude plugin marketplace remove ${n}`).join(' && ');
|
|
261
|
+
return fail(
|
|
262
|
+
`directory marketplace ${leaking.map((n) => `'${n}'`).join(', ')} reads this clone — the plugin cache `
|
|
263
|
+
+ `becomes a full copy incl. .env.local (#1515). Re-register once: ${removes} && `
|
|
264
|
+
+ `claude plugin marketplace add ${stageDir} && claude plugin install ${PLUGIN_NAME}@${packedName}`
|
|
265
|
+
+ (staged.ok ? '' : ` (staging failed: ${staged.detail})`),
|
|
266
|
+
);
|
|
148
267
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
268
|
+
|
|
269
|
+
const pluginId = before.entry.id;
|
|
270
|
+
const marketplace = pluginId.slice(pluginId.indexOf('@') + 1);
|
|
271
|
+
const reg = known?.find((m) => m.name === marketplace);
|
|
272
|
+
const fromStage = reg?.source === 'directory' && !!reg.path && realOrResolved(reg.path) === realOrResolved(stageDir);
|
|
273
|
+
|
|
274
|
+
if (fromStage) {
|
|
275
|
+
const staged = stage();
|
|
276
|
+
if (!staged.ok) return fail(staged.detail);
|
|
277
|
+
} else {
|
|
278
|
+
log(known
|
|
279
|
+
? `claude: marketplace '${marketplace}' reads ${reg?.path ?? reg?.source ?? 'an unknown source'}; to install this clone instead, register the packed copy at ${stageDir}`
|
|
280
|
+
: `claude: ${knownMarketplacesFile} unreadable — cannot check the marketplace source; the packed-copy path is ${stageDir}`);
|
|
154
281
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
282
|
+
|
|
283
|
+
log(`claude: refreshing marketplace '${marketplace}' and plugin ${pluginId}`);
|
|
284
|
+
for (const args of [['plugin', 'marketplace', 'update', marketplace], ['plugin', 'update', pluginId]]) {
|
|
285
|
+
const r = run('claude', args);
|
|
286
|
+
if (!r.ok) return fail(r.detail);
|
|
158
287
|
}
|
|
288
|
+
const after = parseEntry(run('claude', ['plugin', 'list', '--json'], { mutates: false }));
|
|
289
|
+
if (after.error) return fail(after.error);
|
|
290
|
+
const entry = after.entry;
|
|
291
|
+
if (!entry) return fail(`${pluginId} disappeared from plugin list after the update`);
|
|
292
|
+
if (!entry.installPath || !existsSync(entry.installPath)) return fail(`installPath missing: ${entry.installPath}`);
|
|
159
293
|
const missing = missingRuntimeDeps(entry.installPath);
|
|
160
294
|
if (missing.length > 0) {
|
|
161
295
|
const refused = (entry.noteDetails ?? []).some((n) => n?.type === 'dependencies-refused');
|
|
162
296
|
log(`claude: ${missing.length} runtime deps missing${refused ? ' (harness refused the install)' : ''} — installing`);
|
|
163
|
-
|
|
164
|
-
|
|
297
|
+
// The packed copy carries no .npmrc, so the repo's ignore-scripts=true
|
|
298
|
+
// (SEC-020) has to travel as a flag.
|
|
299
|
+
const r = run('npm', ['ci', '--omit=dev', '--ignore-scripts'], { cwd: entry.installPath });
|
|
300
|
+
if (!r.ok) return fail(r.detail);
|
|
165
301
|
const still = opts.dryRun ? [] : missingRuntimeDeps(entry.installPath);
|
|
166
|
-
if (still.length > 0) return
|
|
302
|
+
if (still.length > 0) return fail(`still missing after npm ci: ${still.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
const want = repoVersion(soRoot);
|
|
305
|
+
if (entry.version !== want && fromStage && !opts.dryRun) {
|
|
306
|
+
// The source IS this clone's packed copy, so a different version means
|
|
307
|
+
// the update did not take effect — not that a remote is behind.
|
|
308
|
+
return fail(`update did not take effect: installed ${entry.version}, packed ${want}`);
|
|
167
309
|
}
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
// release is pushed — report it, do not fail on it.
|
|
171
|
-
const lag = entry.version !== want ? `; clone is ${want} — marketplace not yet at it` : '';
|
|
310
|
+
const lag = entry.version !== want && !fromStage
|
|
311
|
+
? `; clone is ${want} — marketplace '${marketplace}' source is not at it yet` : '';
|
|
172
312
|
return {
|
|
173
313
|
step: 'claude',
|
|
174
314
|
status: 'ok',
|
|
@@ -177,7 +317,7 @@ function updateClaude({ run, log }, opts) {
|
|
|
177
317
|
}
|
|
178
318
|
|
|
179
319
|
function updateCodex({ run, log }, opts) {
|
|
180
|
-
if (!
|
|
320
|
+
if (!commandOnPath('codex')) return { step: 'codex', status: 'skipped', detail: 'codex not on PATH' };
|
|
181
321
|
const list = run('codex', ['plugin', 'list'], { mutates: false });
|
|
182
322
|
if (!list.ok) return { step: 'codex', status: 'failed', detail: list.detail };
|
|
183
323
|
if (!new RegExp(`^${PLUGIN_ID}\\s+installed`, 'm').test(list.stdout)) {
|