session-orchestrator 5.7.0 → 5.9.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 +68 -0
- package/README.md +13 -9
- package/agents/eval-judge.md +5 -5
- package/docs/baseline.md +4 -3
- package/docs/codex-setup.md +6 -4
- package/docs/components.md +5 -4
- package/docs/events-schema.md +45 -4
- 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 +2 -2
- package/docs/session-config-reference.md +7 -5
- package/docs/session-config-template.md +3 -2
- 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/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 +2 -2
- 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/ci-status-banner.mjs +38 -5
- 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/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-schema.mjs +8 -0
- package/scripts/lib/events.mjs +19 -5
- package/scripts/lib/fleet-protocol.mjs +98 -1
- package/scripts/lib/instruction-budget-guard.mjs +25 -18
- package/scripts/lib/io.mjs +172 -35
- package/scripts/lib/learnings/candidates.mjs +7 -28
- 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 +3 -5
- 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-wave.mjs +40 -0
- package/scripts/lib/quality-gate.mjs +36 -30
- package/scripts/lib/reconcile/idempotency.mjs +9 -10
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
- 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 +3 -4
- package/scripts/lib/session-id.mjs +2 -1
- package/scripts/lib/session-record-repair.mjs +28 -21
- package/scripts/lib/session-schema/constants.mjs +20 -5
- package/scripts/lib/session-schema/validator.mjs +12 -0
- package/scripts/lib/session-start-probes.mjs +22 -7
- 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 +3 -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 +207 -26
- package/scripts/lib/vault-mirror/utils.mjs +16 -1
- package/scripts/lib/vault-sync-baseline.mjs +9 -8
- package/scripts/lib/wave-executor/remote-dispatch.mjs +27 -9
- package/scripts/lib/wave-resource-gate.mjs +2 -1
- package/scripts/lib/wave-transcript-tail.mjs +8 -4
- 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-learnings-index.mjs +2 -1
- package/scripts/release.mjs +4 -1
- package/scripts/relocate-vault-corpus.mjs +5 -3
- package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
- package/scripts/run-quality-gate.mjs +9 -39
- package/scripts/self-update.mjs +179 -39
- package/scripts/site-numbers.mjs +8 -3
- package/scripts/sweep-expired-learnings.mjs +6 -4
- package/scripts/sync-vault-schema.mjs +3 -7
- package/scripts/windows-setup.mjs +45 -0
- package/skills/_shared/fleet-protocol.md +10 -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 +83 -10
- 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/remote-offload/SKILL.md +1 -1
- package/skills/repo-audit/SKILL.md +2 -2
- package/skills/session-end/SKILL.md +32 -1
- package/skills/session-end/metrics-collection.md +15 -9
- 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 +1 -0
- package/skills/vault-sync/SKILL.md +6 -6
- package/skills/vault-sync/validator.sh +29 -10
- package/skills/wave-executor/references/wave-loop-dispatch.md +1 -1
- package/skills/wave-executor/references/wave-loop-review.md +13 -7
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
- package/skills/write-executable-plan/SKILL.md +1 -0
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
* node scripts/generate-pi-prompts.mjs --check
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync,
|
|
22
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
|
-
import {
|
|
25
|
+
import { isUserInvocableSkill } from './lib/user-invocable-skills.mjs';
|
|
26
|
+
import { listCommandFiles, listSkillDirs, parseFrontmatter } from './lib/plugin-surface.mjs';
|
|
26
27
|
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
29
|
const SCRIPT_DIR = path.dirname(__filename);
|
|
@@ -33,74 +34,6 @@ const PROMPTS_DIR = path.join(ROOT, 'pi', 'prompts');
|
|
|
33
34
|
const CHECK_ONLY = process.argv.includes('--check');
|
|
34
35
|
const DESCRIPTION_MAX = 1024;
|
|
35
36
|
|
|
36
|
-
function isDir(p) {
|
|
37
|
-
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function commandFiles() {
|
|
41
|
-
if (!existsSync(COMMANDS_DIR)) return [];
|
|
42
|
-
return readdirSync(COMMANDS_DIR)
|
|
43
|
-
.filter((name) => name.endsWith('.md'))
|
|
44
|
-
.sort();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function skillDirs() {
|
|
48
|
-
if (!existsSync(SKILLS_DIR)) return [];
|
|
49
|
-
return readdirSync(SKILLS_DIR)
|
|
50
|
-
.filter((name) => isDir(path.join(SKILLS_DIR, name)) && existsSync(path.join(SKILLS_DIR, name, 'SKILL.md')))
|
|
51
|
-
.sort();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Parse YAML-ish frontmatter including `>` / `|` folded scalars, returning
|
|
56
|
-
* DECODED values (surrounding quotes stripped). Skill descriptions are folded
|
|
57
|
-
* scalars in practice, which the previous line-at-a-time parser read as the
|
|
58
|
-
* literal `>`. Mirrors `generate-cursor-adapter.mjs`.
|
|
59
|
-
*
|
|
60
|
-
* A UTF-8 BOM before the opening `---`, or CRLF line endings, used to make the
|
|
61
|
-
* two probes below miss the block entirely: the file parsed as "no
|
|
62
|
-
* frontmatter", so `user-invocable` silently disappeared and the skill was
|
|
63
|
-
* demoted out of `pi/prompts/` with no diagnostic anywhere. Both are normalised
|
|
64
|
-
* away first — the same treatment `parseAgentFrontmatter`
|
|
65
|
-
* (`scripts/lib/agent-frontmatter.mjs`) gives them.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} content
|
|
68
|
-
* @returns {Record<string, string>}
|
|
69
|
-
*/
|
|
70
|
-
function parseFrontmatter(content) {
|
|
71
|
-
const text = (content.charCodeAt(0) === 0xfeff ? content.slice(1) : content).replace(/\r\n?/g, '\n');
|
|
72
|
-
if (!text.startsWith('---\n')) return {};
|
|
73
|
-
const end = text.indexOf('\n---\n', 4);
|
|
74
|
-
if (end === -1) return {};
|
|
75
|
-
|
|
76
|
-
const lines = text.slice(4, end).split('\n');
|
|
77
|
-
const fields = {};
|
|
78
|
-
let i = 0;
|
|
79
|
-
while (i < lines.length) {
|
|
80
|
-
const match = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
81
|
-
if (!match) {
|
|
82
|
-
i += 1;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const key = match[1];
|
|
86
|
-
const raw = match[2];
|
|
87
|
-
if (raw === '>' || raw === '| ' || raw === '|' || raw === '>-' || raw === '|-') {
|
|
88
|
-
const folded = [];
|
|
89
|
-
i += 1;
|
|
90
|
-
while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t') || lines[i] === '')) {
|
|
91
|
-
folded.push(lines[i].replace(/^\s+/, ''));
|
|
92
|
-
i += 1;
|
|
93
|
-
}
|
|
94
|
-
const joiner = raw.startsWith('|') ? '\n' : ' ';
|
|
95
|
-
fields[key] = folded.filter(Boolean).join(joiner).trim();
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
fields[key] = raw.replace(/^["']|["']$/g, '');
|
|
99
|
-
i += 1;
|
|
100
|
-
}
|
|
101
|
-
return fields;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
37
|
/** @see generate-cursor-adapter.mjs — same allow-list, same reasoning. */
|
|
105
38
|
const YAML_SCALAR_LOOKALIKE = /^(?:true|false|yes|no|on|off|null|nan|[-+]?\.?inf|~)$/i;
|
|
106
39
|
const YAML_PLAIN_SAFE = /^[A-Za-z0-9_][A-Za-z0-9 _.,()/-]*$/;
|
|
@@ -138,7 +71,8 @@ function clampDescription(text) {
|
|
|
138
71
|
/**
|
|
139
72
|
* Skills that declare themselves operator-facing slash commands.
|
|
140
73
|
*
|
|
141
|
-
* The predicate is `
|
|
74
|
+
* The predicate is `isUserInvocableSkill` from `scripts/lib/user-invocable-skills.mjs`
|
|
75
|
+
* (absent key = invocable, as Claude Code reads it — #1515; values via `isUserInvocableValue`)
|
|
142
76
|
* — the ONE normaliser for this marker, shared with the Cursor and Codex
|
|
143
77
|
* generators and with every counter. A local copy here is how the quoted-value
|
|
144
78
|
* disagreement arose (`"true"`: wrapper generated on two adapters, counted by
|
|
@@ -147,10 +81,10 @@ function clampDescription(text) {
|
|
|
147
81
|
* @returns {string[]} skill names, sorted
|
|
148
82
|
*/
|
|
149
83
|
function userInvocableSkills() {
|
|
150
|
-
return
|
|
84
|
+
return listSkillDirs(ROOT).filter((name) => {
|
|
151
85
|
const file = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
152
86
|
const fields = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
153
|
-
return
|
|
87
|
+
return isUserInvocableSkill(fields, file);
|
|
154
88
|
});
|
|
155
89
|
}
|
|
156
90
|
|
|
@@ -217,7 +151,7 @@ Read that skill file and follow it exactly. When it references \`$ARGUMENTS\`, s
|
|
|
217
151
|
*/
|
|
218
152
|
function expectedPrompts() {
|
|
219
153
|
const prompts = new Map();
|
|
220
|
-
for (const commandFile of
|
|
154
|
+
for (const commandFile of listCommandFiles(ROOT)) {
|
|
221
155
|
prompts.set(commandFile, renderPrompt(commandFile));
|
|
222
156
|
}
|
|
223
157
|
|
|
@@ -124,6 +124,14 @@ function classifyFailure(err) {
|
|
|
124
124
|
|
|
125
125
|
const stderr = err && typeof err === 'object' ? String(/** @type {any} */ (err).stderr ?? '') : '';
|
|
126
126
|
const haystack = `${message}\n${stderr}`.toLowerCase();
|
|
127
|
+
// Rate limit BEFORE the auth check (#1034 item 2): GitHub answers a primary
|
|
128
|
+
// or secondary rate limit with HTTP 403, so the `http 403` test below would
|
|
129
|
+
// otherwise tell the operator to re-authenticate when waiting is the fix. A
|
|
130
|
+
// 403 without rate-limit wording (expired token, missing scope) stays
|
|
131
|
+
// `auth-error`, and so does an UNAUTHENTICATED rate limit, whose fix is to
|
|
132
|
+
// log in. `abuse detection` is GitHub's older wording for the secondary limit.
|
|
133
|
+
const rateLimited = haystack.includes('rate limit') || haystack.includes('abuse detection');
|
|
134
|
+
if (rateLimited && !haystack.includes('authenticated requests get a higher')) return 'query-failed';
|
|
127
135
|
if (
|
|
128
136
|
haystack.includes('gh auth login') ||
|
|
129
137
|
haystack.includes('not logged in') ||
|
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
* separate helpers. No external deps — Node 20+ stdlib only.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { readFile,
|
|
33
|
+
import { readFile, rename, unlink, mkdir, readdir, stat, lstat } from 'node:fs/promises';
|
|
34
34
|
import { existsSync } from 'node:fs';
|
|
35
35
|
import { randomUUID } from 'node:crypto';
|
|
36
36
|
import path from 'node:path';
|
|
37
|
+
import { atomicWriteText, envelopeToError } from './io.mjs';
|
|
37
38
|
|
|
38
39
|
import { filterRealSessions } from './session-schema.mjs';
|
|
39
40
|
|
|
@@ -284,7 +285,7 @@ export async function shouldDispatchAutoDialectic({
|
|
|
284
285
|
/**
|
|
285
286
|
* Write `.orchestrator/dialectic-last-run` with the given ISO timestamp.
|
|
286
287
|
*
|
|
287
|
-
* Atomicity:
|
|
288
|
+
* Atomicity: `atomicWriteText` (`<dir>/.<name>.tmp.<hex>`, then rename()). Same-fs rename is
|
|
288
289
|
* atomic on POSIX — observers see either the previous file or the new one,
|
|
289
290
|
* never a half-written intermediate (mirrors auto-dream.mjs:248-251).
|
|
290
291
|
*
|
|
@@ -312,9 +313,10 @@ export async function writeDialecticLastRun({ repoRoot, isoTimestamp } = {}) {
|
|
|
312
313
|
const target = lastRunPath(repoRoot);
|
|
313
314
|
try {
|
|
314
315
|
await mkdir(path.dirname(target), { recursive: true });
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
const res = await atomicWriteText(target, `${isoTimestamp}\n`, {
|
|
317
|
+
tmpPrefix: `.${path.basename(target)}.tmp`,
|
|
318
|
+
});
|
|
319
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
318
320
|
return { ok: true, path: target };
|
|
319
321
|
} catch (err) {
|
|
320
322
|
return { ok: false, error: err.message };
|
|
@@ -401,11 +403,10 @@ export async function writeDialecticPending({
|
|
|
401
403
|
].join('\n');
|
|
402
404
|
|
|
403
405
|
const content = `${frontmatter}${diff}${diff.endsWith('\n') ? '' : '\n'}`;
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
await rename(tmp, target);
|
|
406
|
+
const res = await atomicWriteText(target, content, { tmpPrefix: `.${path.basename(target)}.tmp` });
|
|
407
|
+
if (!res.ok) throw envelopeToError(res);
|
|
407
408
|
|
|
408
|
-
return { path: target, bytes:
|
|
409
|
+
return { path: target, bytes: res.bytes };
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
/**
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
* No external deps — Node 20+ stdlib only.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readFile,
|
|
24
|
+
import { readFile, unlink, mkdir } from 'node:fs/promises';
|
|
25
25
|
import { existsSync, statSync } from 'node:fs';
|
|
26
|
-
import { randomUUID } from 'node:crypto';
|
|
27
26
|
import path from 'node:path';
|
|
27
|
+
import { atomicWriteText, envelopeToError } from './io.mjs';
|
|
28
28
|
|
|
29
29
|
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
30
30
|
import { filterRealSessions } from './session-schema.mjs';
|
|
@@ -205,7 +205,7 @@ function pendingDreamPath(repoRoot) {
|
|
|
205
205
|
* the metadata session-end's Final Report and the next session's --apply-pending
|
|
206
206
|
* step both rely on.
|
|
207
207
|
*
|
|
208
|
-
* Atomicity:
|
|
208
|
+
* Atomicity: `atomicWriteText` (`<dir>/.<name>.tmp.<hex>`, then rename()). Same-fs rename is
|
|
209
209
|
* atomic on POSIX — observers see either the previous file or the new one,
|
|
210
210
|
* never a half-written intermediate.
|
|
211
211
|
*
|
|
@@ -245,11 +245,10 @@ export async function writePendingDream({
|
|
|
245
245
|
].join('\n');
|
|
246
246
|
|
|
247
247
|
const content = `${frontmatter}${diff}${diff.endsWith('\n') ? '' : '\n'}`;
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
await rename(tmp, target);
|
|
248
|
+
const res = await atomicWriteText(target, content, { tmpPrefix: `.${path.basename(target)}.tmp` });
|
|
249
|
+
if (!res.ok) throw envelopeToError(res);
|
|
251
250
|
|
|
252
|
-
return { path: target, bytes:
|
|
251
|
+
return { path: target, bytes: res.bytes };
|
|
253
252
|
}
|
|
254
253
|
|
|
255
254
|
/**
|
|
@@ -472,9 +471,8 @@ export async function applyPendingDream({ repoRoot, memoryDir, maxAgeDays = 14 }
|
|
|
472
471
|
|
|
473
472
|
const newBody = extractedBlock.trimEnd() + '\n';
|
|
474
473
|
await mkdir(path.dirname(memoryPath), { recursive: true });
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
await rename(tmp, memoryPath);
|
|
474
|
+
const res = await atomicWriteText(memoryPath, newBody, { tmpPrefix: '.MEMORY.md.tmp' });
|
|
475
|
+
if (!res.ok) throw envelopeToError(res);
|
|
478
476
|
|
|
479
477
|
const linesAfter = newBody.length === 0 ? 0 : newBody.split('\n').length;
|
|
480
478
|
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* linkChildLoopToCoordinator(childRunId, parentRunId)— Phase D linkage documentation helper
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
import { atomicWriteWithBackup, envelopeToError } from '../io.mjs';
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Schema version
|
|
@@ -38,7 +39,11 @@ export const SCHEMA_VERSION = 1;
|
|
|
38
39
|
* Crash-safe: a partial tmpfile is never visible at the destination path.
|
|
39
40
|
* POSIX rename(2) is atomic within the same filesystem.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
+
* Write path: `atomicWriteWithBackup` (`../io.mjs`), which also creates a
|
|
43
|
+
* missing parent directory (mkdir -p) and unlinks its tmp on failure. The tmp
|
|
44
|
+
* keeps the `<file>.tmp-<pid>.<hex>` shape so the `*.jsonl.tmp-*` crash-residue
|
|
45
|
+
* ignore pattern (bootstrap store-lock-ignore) still matches it. Throws on
|
|
46
|
+
* failure with the original message and `code` (`envelopeToError`).
|
|
42
47
|
*
|
|
43
48
|
* @param {object} record — the record to serialize as a JSONL line
|
|
44
49
|
* @param {string} jsonlPath — destination JSONL path
|
|
@@ -53,9 +58,10 @@ export function appendJsonlAtomic(record, jsonlPath) {
|
|
|
53
58
|
if (existing.length > 0 && !existing.endsWith('\n')) existing += '\n';
|
|
54
59
|
|
|
55
60
|
const line = JSON.stringify(record) + '\n';
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const res = atomicWriteWithBackup(jsonlPath, existing + line, {
|
|
62
|
+
tmpPrefix: `${path.basename(jsonlPath)}.tmp-${process.pid}`,
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) throw envelopeToError(res);
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
// ---------------------------------------------------------------------------
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
* — a missing or structurally invalid lock is refused, not synthesized.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { existsSync, readFileSync
|
|
26
|
-
import { join
|
|
27
|
-
import {
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { atomicWriteWithBackup, envelopeToError } from './io.mjs';
|
|
28
28
|
import { parseBootstrapLock } from './bootstrap-lock-freshness.mjs';
|
|
29
29
|
|
|
30
30
|
const PRECONDITION_MESSAGE_SUFFIX =
|
|
@@ -84,20 +84,16 @@ function sanitizeLockValue(value) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Atomically write `content` to `filePath` via tmp-file +
|
|
88
|
-
*
|
|
89
|
-
* and `
|
|
87
|
+
* Atomically write `content` to `filePath` via tmp-file + rename
|
|
88
|
+
* (`scripts/lib/io.mjs#atomicWriteWithBackup`). Throws on failure with the
|
|
89
|
+
* original message and `code` (`envelopeToError`).
|
|
90
90
|
*
|
|
91
91
|
* @param {string} filePath
|
|
92
92
|
* @param {string} content
|
|
93
93
|
*/
|
|
94
94
|
function writeTextAtomicSync(filePath, content) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const tmpSuffix = randomBytes(6).toString('hex');
|
|
98
|
-
const tmpFile = join(dir, `.bootstrap-lock-refresh.${tmpSuffix}.tmp`);
|
|
99
|
-
writeFileSync(tmpFile, content, { encoding: 'utf8' });
|
|
100
|
-
renameSync(tmpFile, filePath);
|
|
95
|
+
const res = atomicWriteWithBackup(filePath, content, { tmpPrefix: '.bootstrap-lock-refresh.tmp' });
|
|
96
|
+
if (!res.ok) throw envelopeToError(res);
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
/**
|
|
@@ -1089,8 +1089,23 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
1089
1089
|
};
|
|
1090
1090
|
}
|
|
1091
1091
|
|
|
1092
|
+
// #856: lifecycle and conclusion are separate facts. Failure retains
|
|
1093
|
+
// priority even in an inconsistent row; only completed success grants green.
|
|
1094
|
+
// Counts are additive diagnostics, never a new status or gate authority.
|
|
1095
|
+
const checkRunCounts = { success: 0, failure: 0, inProgress: 0, queued: 0, cancelled: 0, skipped: 0, neutral: 0, waiting: 0, other: 0 };
|
|
1096
|
+
for (const run of checkRuns) {
|
|
1097
|
+
if (run?.conclusion === 'failure' || run?.conclusion === 'action_required') checkRunCounts.failure++;
|
|
1098
|
+
else if (run?.status === 'completed' && run?.conclusion === 'success') checkRunCounts.success++;
|
|
1099
|
+
else if (run?.status === 'completed' && run?.conclusion === 'cancelled') checkRunCounts.cancelled++;
|
|
1100
|
+
else if (run?.status === 'completed' && run?.conclusion === 'skipped') checkRunCounts.skipped++;
|
|
1101
|
+
else if (run?.status === 'completed' && run?.conclusion === 'neutral') checkRunCounts.neutral++;
|
|
1102
|
+
else if (['waiting', 'pending', 'requested'].includes(run?.status) && run?.conclusion === null) checkRunCounts.waiting++;
|
|
1103
|
+
else if (run?.status === 'in_progress' && run?.conclusion === null) checkRunCounts.inProgress++;
|
|
1104
|
+
else if (run?.status === 'queued' && run?.conclusion === null) checkRunCounts.queued++;
|
|
1105
|
+
else checkRunCounts.other++;
|
|
1106
|
+
}
|
|
1092
1107
|
const failedRun = checkRuns.find(
|
|
1093
|
-
(r) => r
|
|
1108
|
+
(r) => r?.conclusion === 'failure' || r?.conclusion === 'action_required',
|
|
1094
1109
|
);
|
|
1095
1110
|
|
|
1096
1111
|
if (failedRun) {
|
|
@@ -1101,11 +1116,12 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
1101
1116
|
details: {
|
|
1102
1117
|
cliUsed: 'gh',
|
|
1103
1118
|
reason: 'lastGreen-not-implemented-for-github',
|
|
1119
|
+
checkRunCounts,
|
|
1104
1120
|
},
|
|
1105
1121
|
};
|
|
1106
1122
|
}
|
|
1107
1123
|
|
|
1108
|
-
const allSuccess =
|
|
1124
|
+
const allSuccess = checkRunCounts.success === checkRuns.length;
|
|
1109
1125
|
if (allSuccess) {
|
|
1110
1126
|
return {
|
|
1111
1127
|
status: 'green',
|
|
@@ -1116,13 +1132,24 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
1116
1132
|
};
|
|
1117
1133
|
}
|
|
1118
1134
|
|
|
1119
|
-
|
|
1135
|
+
const states = [
|
|
1136
|
+
['inProgress', 'check-runs-in-progress'],
|
|
1137
|
+
['queued', 'check-runs-queued'],
|
|
1138
|
+
['cancelled', 'check-runs-cancelled'],
|
|
1139
|
+
['skipped', 'check-runs-skipped'],
|
|
1140
|
+
['neutral', 'check-runs-neutral'],
|
|
1141
|
+
['waiting', 'check-runs-waiting'],
|
|
1142
|
+
['other', 'check-runs-unrecognised'],
|
|
1143
|
+
].filter(([key]) => checkRunCounts[key] > 0);
|
|
1144
|
+
// Successes may accompany one unknown state; counts expose the whole mix.
|
|
1145
|
+
const reason = states.length === 1 ? states[0][1] : 'check-runs-mixed';
|
|
1120
1146
|
return {
|
|
1121
1147
|
status: 'unknown',
|
|
1122
1148
|
ok: false,
|
|
1123
1149
|
details: {
|
|
1124
1150
|
cliUsed: 'gh',
|
|
1125
|
-
reason
|
|
1151
|
+
reason,
|
|
1152
|
+
checkRunCounts,
|
|
1126
1153
|
},
|
|
1127
1154
|
};
|
|
1128
1155
|
}
|
|
@@ -1189,6 +1216,7 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
1189
1216
|
* cliUsed: 'glab'|'gh',
|
|
1190
1217
|
* reason?: string,
|
|
1191
1218
|
* error?: string,
|
|
1219
|
+
* checkRunCounts?: { success: number, failure: number, inProgress: number, queued: number, cancelled: number, skipped: number, neutral: number, waiting: number, other: number },
|
|
1192
1220
|
* matchedRef?: string,
|
|
1193
1221
|
* candidateCount?: number,
|
|
1194
1222
|
* candidateStatuses?: string[],
|
|
@@ -1202,7 +1230,12 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
1202
1230
|
* #857 "several contradicting pipelines" and "only foreign-ref pipelines"
|
|
1203
1231
|
* findings — and the #1390 "same-sha rows the ref preference set aside"
|
|
1204
1232
|
* (`droppedCount`/`droppedStatuses`) — are delivered through ADDITIVE `details`
|
|
1205
|
-
* fields and `details.reason
|
|
1233
|
+
* fields and `details.reason`. GitHub's `checkRunCounts` counts each row once:
|
|
1234
|
+
* `failure` includes action_required; `other` includes malformed/inconsistent
|
|
1235
|
+
* rows and unsupported statuses/conclusions. Non-success reasons are
|
|
1236
|
+
* check-runs-in-progress, check-runs-queued, check-runs-cancelled,
|
|
1237
|
+
* check-runs-skipped, check-runs-neutral, check-runs-waiting (#1526),
|
|
1238
|
+
* check-runs-unrecognised, or check-runs-mixed (multiple non-success kinds),
|
|
1206
1239
|
* never as a new status value — consumers fail open on an unknown status string
|
|
1207
1240
|
* and the session-start renderer prints nothing for one.
|
|
1208
1241
|
*/
|
|
@@ -14,7 +14,7 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
14
14
|
* GENERIC over any consumer repo, where a slash command still IS a
|
|
15
15
|
* `commands/*.md` file. The 2026-09-16 command→skill fold (#1370) is a fact
|
|
16
16
|
* about THIS plugin — 24 command files folded into their same-named
|
|
17
|
-
* `skills/<name>/SKILL.md` behind
|
|
17
|
+
* `skills/<name>/SKILL.md` behind `user-invocable: true` (absent counts as true, #1515) — so
|
|
18
18
|
* teaching the shared checker that union would mis-measure every repo that never
|
|
19
19
|
* folded. `scripts/site-numbers.mjs` `countCommands()` carries the union for this
|
|
20
20
|
* plugin's own tile, and names the divergence in its docblock.
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* Tolerant parser: any other value (or an absent block) resolves to disabled.
|
|
14
14
|
*
|
|
15
15
|
* Consumer: `hooks/post-tooluse-frontend-slop.mjs`.
|
|
16
|
+
*
|
|
17
|
+
* @deprecated since 5.8.0 (#1121) — the hook this block drives is removed in
|
|
18
|
+
* the next major. The parser stays side-effect free; the hook prints the
|
|
19
|
+
* deprecation notice when the block is enabled.
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { promises as fs } from 'node:fs';
|
|
@@ -32,6 +36,7 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
32
36
|
*
|
|
33
37
|
* @param {string} content — full file contents
|
|
34
38
|
* @returns {{ enabled: boolean }}
|
|
39
|
+
* @deprecated since 5.8.0 (#1121) — removal in the next major.
|
|
35
40
|
*/
|
|
36
41
|
export function _parseFrontendSlopHook(content) {
|
|
37
42
|
const defaults = { enabled: false };
|
|
@@ -85,6 +90,7 @@ export function _parseFrontendSlopHook(content) {
|
|
|
85
90
|
*
|
|
86
91
|
* @param {{ repoRoot?: string }} [opts]
|
|
87
92
|
* @returns {Promise<{ enabled: boolean }>}
|
|
93
|
+
* @deprecated since 5.8.0 (#1121) — removal in the next major.
|
|
88
94
|
*/
|
|
89
95
|
export async function loadFrontendSlopHookConfig(opts = {}) {
|
|
90
96
|
// Called at USE time, never at module load: the deprecated `SO_PROJECT_DIR`
|
package/scripts/lib/config.mjs
CHANGED
|
@@ -186,7 +186,11 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
186
186
|
// String fields
|
|
187
187
|
const vcs = _coerceString(kv, 'vcs', undefined);
|
|
188
188
|
const gitlabHost = _coerceString(kv, 'gitlab-host', undefined);
|
|
189
|
-
|
|
189
|
+
// `mirror` keeps an explicit `none` as the string 'none' instead of the
|
|
190
|
+
// generic _coerceString null (#1034): session-end Phase 4.4 must tell an
|
|
191
|
+
// explicit opt-out (`mirror: none` → never push) from an absent key
|
|
192
|
+
// (null → push when a `github` remote exists, the pre-#1034 behaviour).
|
|
193
|
+
const mirror = _getVal(kv, 'mirror', undefined) === 'none' ? 'none' : _coerceString(kv, 'mirror', undefined);
|
|
190
194
|
const special = _coerceString(kv, 'special', undefined);
|
|
191
195
|
const pencil = _coerceString(kv, 'pencil', undefined);
|
|
192
196
|
const testCommand = _coerceString(kv, 'test-command', 'npm test');
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
47
|
-
import {
|
|
47
|
+
import { eventsFilePath } from './events.mjs';
|
|
48
48
|
import { isMainModule } from './is-main-module.mjs';
|
|
49
|
+
import { getProjectDir } from './platform.mjs';
|
|
49
50
|
|
|
50
51
|
const DEFAULT_INTERVAL_S = 2;
|
|
51
52
|
const EVENTS_FILE_REL = '.orchestrator/metrics/events.jsonl';
|
|
@@ -454,8 +455,9 @@ function sleep(ms) {
|
|
|
454
455
|
* @param {number} intervalS
|
|
455
456
|
*/
|
|
456
457
|
async function tailLoop(intervalS) {
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
// #1517: the PROJECT root, never CLAUDE_PLUGIN_ROOT (the installed plugin
|
|
459
|
+
// directory, whose ledger is not this repo's).
|
|
460
|
+
const absPath = eventsFilePath(getProjectDir()); // #1514: the repo's one ledger
|
|
459
461
|
/** @type {Map<number, WaveSummary>} */
|
|
460
462
|
const state = new Map();
|
|
461
463
|
/** @type {Set<string>} */
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
|
|
40
40
|
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
41
41
|
import { join } from 'node:path';
|
|
42
|
+
import { getProjectDir } from './platform.mjs';
|
|
42
43
|
|
|
43
44
|
// Ceiling (BV-004): one process per session that invoked session-start
|
|
44
45
|
// (monitors/monitors.json `when: on-skill-invoke:session-start`), alive until
|
|
45
46
|
// SIGTERM; one stat + one full readFileSync of ONE local file per 900 s
|
|
46
|
-
// (resolved against
|
|
47
|
+
// (resolved against the project dir via getProjectDir(), #1517); no network, no repo
|
|
47
48
|
// enumeration, no Session Config read (runs even with `ecosystem-health: false`).
|
|
48
49
|
// The watched file has NO producer (0 writers, measured 2026-09-18:
|
|
49
50
|
// `rg -l "ecosystem-health\.jsonl"` → this file only) — every run emits
|
|
@@ -129,8 +130,9 @@ function fingerprint(absPath) {
|
|
|
129
130
|
* @param {number} intervalS
|
|
130
131
|
*/
|
|
131
132
|
async function watchLoop(intervalS) {
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
// #1517: the PROJECT root, never CLAUDE_PLUGIN_ROOT (the installed plugin
|
|
134
|
+
// directory, whose metrics are not this repo's).
|
|
135
|
+
const absPath = join(getProjectDir(), STATE_FILE_REL);
|
|
134
136
|
|
|
135
137
|
// Startup event — confirms the watcher is alive even when nothing has
|
|
136
138
|
// happened yet. Without this, a "no events" stream is indistinguishable
|