session-orchestrator 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/CHANGELOG.md +494 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +1 -1
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/release.md +60 -0
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +1 -1
- package/docs/instruction-delivery.md +350 -0
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +1 -41
- package/docs/session-config-template.md +0 -23
- package/hooks/_lib/guard-source-loader.mjs +304 -91
- package/hooks/enforce-commands.mjs +216 -17
- package/hooks/enforce-scope.mjs +236 -12
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +52 -5
- package/hooks/on-session-start.mjs +7 -4
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-bash-destructive-guard.mjs +146 -59
- package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +2 -2
- package/pi/prompts/release.md +12 -0
- package/scripts/autopilot.mjs +3 -1
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/emit-session.mjs +45 -40
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +207 -23
- package/scripts/lib/command-blocker.mjs +322 -62
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +193 -7
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/select.mjs +704 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/reconcile/emitter.mjs +107 -22
- package/scripts/lib/reconcile/engine.mjs +9 -15
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +134 -1
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +852 -72
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-close-backfill.mjs +2 -2
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/state-md/mission-status.mjs +21 -12
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +188 -20
- package/scripts/lib/validate/check-rules.mjs +31 -5
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +757 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +500 -19
- package/scripts/print-applicable-rules.mjs +170 -7
- package/scripts/print-learnings-index.mjs +501 -0
- package/scripts/release.mjs +616 -61
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +82 -0
- package/scripts/validate-wave-scope.mjs +281 -12
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/state-ownership.md +17 -0
- package/skills/brainstorm/soul.md +47 -1
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/evolve/SKILL.md +116 -18
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +54 -39
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/soul.md +44 -1
- package/skills/memory-cleanup/SKILL.md +18 -5
- package/skills/npm-publish/SKILL.md +22 -50
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/mode-new.md +23 -5
- package/skills/plan/soul.md +46 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +45 -26
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +30 -1
- package/skills/session-end/plan-verification.md +1 -5
- package/skills/session-end/session-metrics-write.md +6 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +15 -1
- package/skills/session-start/soul.md +41 -1
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/SKILL.md +1 -5
- package/skills/wave-executor/wave-loop.md +77 -82
- package/scripts/lib/mission-status-schema.mjs +0 -114
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* node scripts/validate-wave-scope.mjs <path-to-wave-scope.json>
|
|
10
10
|
* cat wave-scope.json | node scripts/validate-wave-scope.mjs
|
|
11
11
|
* node scripts/validate-wave-scope.mjs --assert-subset <agent-filescope.json> < wave-scope.json
|
|
12
|
+
* node scripts/validate-wave-scope.mjs --assert-disjoint <agent-scopes.json> < wave-scope.json
|
|
13
|
+
* node scripts/validate-wave-scope.mjs --union <agent-scopes.json> < wave-scope.json
|
|
12
14
|
*
|
|
13
15
|
* Flags:
|
|
14
16
|
* --assert-subset <path> After schema validation passes, read the agent
|
|
@@ -26,21 +28,68 @@
|
|
|
26
28
|
* the caller may pass the flag unconditionally on
|
|
27
29
|
* every pre-dispatch check. A skip is announced on
|
|
28
30
|
* stderr as a WARN.
|
|
31
|
+
* --assert-disjoint <p> #1020. Read the wave's per-agent scope SIDECAR and
|
|
32
|
+
* assert no file is claimed by two agents of the same
|
|
33
|
+
* wave. Fails (exit 1) with one message per collision.
|
|
34
|
+
* `knownFiles` for the glob∩glob stage comes from
|
|
35
|
+
* `git ls-files` — spawned HERE, in the CLI layer,
|
|
36
|
+
* because scripts/lib/scope-gate.mjs is hook-safe and
|
|
37
|
+
* must not spawn a process (see its module header).
|
|
38
|
+
* --union <path> #1020. QUERY MODE. Read the same sidecar, compute
|
|
39
|
+
* `expandTestSiblings(unionFileScopes(scopes), {role})`
|
|
40
|
+
* using the MANIFEST'S OWN `role`, and print the
|
|
41
|
+
* resulting allowedPaths array as JSON on stdout.
|
|
42
|
+
* Mechanical replacement for the "Collect all file
|
|
43
|
+
* paths … Deduplicate entries" prose in
|
|
44
|
+
* skills/wave-executor/wave-loop.md § Scope Manifest #3.
|
|
45
|
+
*
|
|
46
|
+
* ## SIDECAR FORMAT (both #1020 flags) — an ARRAY, never an object map
|
|
47
|
+
* [{ "id": "W2-C1", "files": ["scripts/a.mjs"] }, { "id": "W2-C4", "files": [...] }]
|
|
48
|
+
* An object keyed by agent id would swallow a DUPLICATE agent id silently, and a
|
|
49
|
+
* duplicated id is a real copy-paste failure mode (it hides one agent's scope
|
|
50
|
+
* from every per-agent check). The array form keeps both records, and
|
|
51
|
+
* `findScopeCollisions` reports the duplicate as its own finding.
|
|
52
|
+
*
|
|
53
|
+
* ## STDOUT CONTRACT (why --union suppresses the manifest echo)
|
|
54
|
+
* Without `--union` this script writes EXACTLY ONE thing to stdout: the input
|
|
55
|
+
* manifest, echoed back verbatim. Callers rely on that — `JSON.parse(stdout)`.
|
|
56
|
+
* `--union` is the first mode that has something else to say, so it is a pure
|
|
57
|
+
* QUERY MODE: it REPLACES the echo rather than adding to it, and stdout carries
|
|
58
|
+
* only the computed allowedPaths array. Mixing both on stdout would break every
|
|
59
|
+
* `JSON.parse(stdout)` caller; writing the union to a second sink would need a
|
|
60
|
+
* file argument the caller must then read back. One JSON document per run, and
|
|
61
|
+
* the flag decides which one.
|
|
29
62
|
*
|
|
30
63
|
* Exit codes:
|
|
31
|
-
* 0 — valid (validated JSON echoed to stdout)
|
|
32
|
-
* 1 — invalid input / validation failure (error messages written to stderr)
|
|
33
|
-
*
|
|
64
|
+
* 0 — valid (validated JSON echoed to stdout; with --union: the union array)
|
|
65
|
+
* 1 — invalid input / validation failure (error messages written to stderr).
|
|
66
|
+
* A scope COLLISION is a validation finding, exactly like the #796 subset
|
|
67
|
+
* and #970 test-sibling violations — the collision-vs-subset distinction
|
|
68
|
+
* lives in the MESSAGE, not in a new exit code.
|
|
69
|
+
* 2 — I/O error (file not found, unreadable stdin, unreadable sidecar file)
|
|
34
70
|
*/
|
|
35
71
|
|
|
36
72
|
import path from 'node:path';
|
|
37
73
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
74
|
+
import { execFileSync } from 'node:child_process';
|
|
38
75
|
import { warn } from './lib/common.mjs';
|
|
39
76
|
import {
|
|
40
77
|
assertFileScopeSubset,
|
|
41
78
|
assertTestSiblingCoverage,
|
|
42
79
|
testSiblingExpansionApplies,
|
|
43
80
|
TEST_SIBLING_EXPANSION_ROLES,
|
|
81
|
+
findScopeCollisions,
|
|
82
|
+
unionFileScopes,
|
|
83
|
+
// #1057 — the read-only-role predicate + THE list. Imported rather than
|
|
84
|
+
// re-listed so the validator and hooks/enforce-scope.mjs cannot disagree about
|
|
85
|
+
// which roles are allowed to grant zero paths.
|
|
86
|
+
isReadOnlyWaveRole,
|
|
87
|
+
READ_ONLY_WAVE_ROLES,
|
|
88
|
+
// Aliased: `expandTestSiblings` is ALSO the name of the pre-existing
|
|
89
|
+
// boolean parameter threaded through validate()/assertSubsetOrDie for the
|
|
90
|
+
// #970 flag. Aliasing the import avoids shadowing that parameter rather than
|
|
91
|
+
// renaming it — the #970 call path stays byte-identical.
|
|
92
|
+
expandTestSiblings as expandScopeTestSiblings,
|
|
44
93
|
} from './lib/scope-gate.mjs';
|
|
45
94
|
|
|
46
95
|
/**
|
|
@@ -54,21 +103,53 @@ function die(msg, code = 1) {
|
|
|
54
103
|
process.exit(code);
|
|
55
104
|
}
|
|
56
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Read the value operand of a value-taking flag, REFUSING one that is itself a
|
|
108
|
+
* flag. Used by the #1020 flags only.
|
|
109
|
+
*
|
|
110
|
+
* `--assert-subset` (#796) consumes `argv[i + 1]` BLIND, so
|
|
111
|
+
* `--assert-subset --assert-disjoint x.json` reads `--assert-disjoint` as its
|
|
112
|
+
* path value. That behaviour is deliberately left untouched — its no-value
|
|
113
|
+
* message is pinned byte-for-byte by
|
|
114
|
+
* tests/scripts/validate-wave-scope.test.mjs — but the flags added here do not
|
|
115
|
+
* inherit it: a swallowed flag is silent (the mode never runs, and the caller
|
|
116
|
+
* believes it did), whereas this refusal is loud and one line long.
|
|
117
|
+
*
|
|
118
|
+
* @param {string[]} argv
|
|
119
|
+
* @param {number} i - index of the FLAG token
|
|
120
|
+
* @param {string} flag - the flag name, for the error message
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
function flagValue(argv, i, flag) {
|
|
124
|
+
const value = argv[i + 1];
|
|
125
|
+
if (value === undefined || value.startsWith('--')) {
|
|
126
|
+
die(`${flag} requires a file-path argument`, 1);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
57
131
|
/**
|
|
58
132
|
* Parse CLI flags out of argv, leaving positional args behind.
|
|
59
133
|
*
|
|
60
|
-
* Recognised: `--assert-subset <path>` (#796)
|
|
61
|
-
* (#
|
|
62
|
-
* wave-scope.json file path), preserving
|
|
63
|
-
* input file.
|
|
134
|
+
* Recognised: `--assert-subset <path>` (#796), `--expand-test-siblings` (#970),
|
|
135
|
+
* `--assert-disjoint <path>` and `--union <path>` (#1020). Everything else is
|
|
136
|
+
* treated as a positional argument (the wave-scope.json file path), preserving
|
|
137
|
+
* legacy behaviour where argv[2] is the input file.
|
|
138
|
+
*
|
|
139
|
+
* The #1020 branches sit BEFORE the positional fallback, as their own `else if`
|
|
140
|
+
* arms: routed through the fallback instead, `--assert-disjoint` would be read
|
|
141
|
+
* as a wave-scope.json path and the mode would never run.
|
|
64
142
|
*
|
|
65
143
|
* @param {string[]} argv - full process.argv
|
|
66
|
-
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean,
|
|
144
|
+
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean,
|
|
145
|
+
* assertDisjoint: string|null, union: string|null, positionals: string[] }}
|
|
67
146
|
*/
|
|
68
147
|
function parseArgs(argv) {
|
|
69
148
|
const positionals = [];
|
|
70
149
|
let assertSubset = null;
|
|
71
150
|
let expandTestSiblings = false;
|
|
151
|
+
let assertDisjoint = null;
|
|
152
|
+
let union = null;
|
|
72
153
|
for (let i = 2; i < argv.length; i++) {
|
|
73
154
|
const a = argv[i];
|
|
74
155
|
if (a === '--expand-test-siblings') {
|
|
@@ -79,11 +160,17 @@ function parseArgs(argv) {
|
|
|
79
160
|
die('--assert-subset requires a file-path argument', 1);
|
|
80
161
|
}
|
|
81
162
|
i++; // consume the value
|
|
163
|
+
} else if (a === '--assert-disjoint') {
|
|
164
|
+
assertDisjoint = flagValue(argv, i, '--assert-disjoint');
|
|
165
|
+
i++; // consume the value
|
|
166
|
+
} else if (a === '--union') {
|
|
167
|
+
union = flagValue(argv, i, '--union');
|
|
168
|
+
i++; // consume the value
|
|
82
169
|
} else {
|
|
83
170
|
positionals.push(a);
|
|
84
171
|
}
|
|
85
172
|
}
|
|
86
|
-
return { assertSubset, expandTestSiblings, positionals };
|
|
173
|
+
return { assertSubset, expandTestSiblings, assertDisjoint, union, positionals };
|
|
87
174
|
}
|
|
88
175
|
|
|
89
176
|
/**
|
|
@@ -259,6 +346,27 @@ function validateAllowedPaths(obj, errors, warnings) {
|
|
|
259
346
|
errors.push(`allowedPaths must be an array, got type: ${ap === null ? 'null' : typeof ap}`);
|
|
260
347
|
return;
|
|
261
348
|
}
|
|
349
|
+
// #1057 — an empty union under a WRITABLE role. WARN, never error, and the
|
|
350
|
+
// distinction is measured rather than stylistic: `skills/wave-executor/wave-loop.md`
|
|
351
|
+
// § Scope Manifest deliberately feeds a skeleton with `"allowedPaths": []`
|
|
352
|
+
// through THIS validator in `--assert-disjoint` and `--union` mode, BEFORE the
|
|
353
|
+
// union exists to be written. An error would break the documented procedure
|
|
354
|
+
// that produces the very field it complains about.
|
|
355
|
+
//
|
|
356
|
+
// Named ceiling (BV-004): the warning therefore also fires on that legitimate
|
|
357
|
+
// skeleton run — one stderr line on a happy path, accepted because the
|
|
358
|
+
// alternative is a mode-conditional warning, i.e. a second place that has to
|
|
359
|
+
// enumerate the modes correctly. Revisit if a third empty-skeleton mode lands.
|
|
360
|
+
if (ap.length === 0 && typeof obj.role === 'string' && obj.role.trim().length > 0
|
|
361
|
+
&& !isReadOnlyWaveRole(obj.role)) {
|
|
362
|
+
warnings.push(
|
|
363
|
+
`allowedPaths is empty for role "${obj.role}" — every write in this wave will be DENIED by ` +
|
|
364
|
+
`hooks/enforce-scope.mjs. Empty is intentional only for a read-only role ` +
|
|
365
|
+
`(${READ_ONLY_WAVE_ROLES.join(', ')}); for a writable role it usually means the coordinator's ` +
|
|
366
|
+
`--union step did not complete. Expected while validating the pre-union skeleton; otherwise ` +
|
|
367
|
+
`re-run --union and rewrite the manifest.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
262
370
|
for (const entry of ap) {
|
|
263
371
|
if (typeof entry !== 'string' || entry.length === 0) {
|
|
264
372
|
errors.push('allowedPaths contains empty string');
|
|
@@ -414,13 +522,157 @@ function assertSubsetOrDie(obj, fileScopePath, expandTestSiblings = false) {
|
|
|
414
522
|
}
|
|
415
523
|
}
|
|
416
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Read + shape-check the per-agent scope SIDECAR shared by `--assert-disjoint`
|
|
527
|
+
* and `--union` (#1020). Exits on any defect; returns the records on success.
|
|
528
|
+
*
|
|
529
|
+
* Exit codes mirror {@link assertSubsetOrDie} exactly: 2 for I/O (missing, not a
|
|
530
|
+
* regular file, unreadable), 1 for every content defect.
|
|
531
|
+
*
|
|
532
|
+
* ## Why the shape check is STRICTER than the library's tolerance
|
|
533
|
+
* `findScopeCollisions` / `unionFileScopes` are fail-closed and never throw:
|
|
534
|
+
* they SKIP a member that is not an object, and treat a missing `files` as `[]`.
|
|
535
|
+
* That is right for a hook-hot-path primitive and wrong for a CLI. A sidecar
|
|
536
|
+
* that spells the key `file:` instead of `files:` would then contribute nothing
|
|
537
|
+
* and both modes would report success on a scope that silently vanished — a
|
|
538
|
+
* path the operator NAMED and the tool did not honour. The absent-input guard
|
|
539
|
+
* belongs in the CLI layer (recorded learning, conf 0.80: a tolerant reader
|
|
540
|
+
* cannot carry a CLI's absent-input guard), so `files` is REQUIRED here.
|
|
541
|
+
*
|
|
542
|
+
* `id` is deliberately NOT required: scope-gate's `normalizeAgentScopes` runs a
|
|
543
|
+
* record with no usable id as `<unnamed#i>` rather than dropping it, precisely
|
|
544
|
+
* because an unreviewed scope is the one that collides. Requiring it here would
|
|
545
|
+
* reject exactly the input that contract was written to keep.
|
|
546
|
+
*
|
|
547
|
+
* @param {string} sidecarPath
|
|
548
|
+
* @param {string} flag - the flag name, for error messages
|
|
549
|
+
* @returns {Array<{id?: string, files: string[]}>}
|
|
550
|
+
*/
|
|
551
|
+
function readAgentScopesOrDie(sidecarPath, flag) {
|
|
552
|
+
if (!existsSync(sidecarPath) || !statSync(sidecarPath).isFile()) {
|
|
553
|
+
die(`Cannot read ${flag} file: ${sidecarPath}`, 2);
|
|
554
|
+
}
|
|
555
|
+
let raw;
|
|
556
|
+
try {
|
|
557
|
+
raw = readFileSync(sidecarPath, 'utf8');
|
|
558
|
+
} catch (err) {
|
|
559
|
+
die(`Cannot read ${flag} file ${sidecarPath}: ${err.message}`, 2);
|
|
560
|
+
}
|
|
561
|
+
let parsed;
|
|
562
|
+
try {
|
|
563
|
+
parsed = JSON.parse(raw);
|
|
564
|
+
} catch {
|
|
565
|
+
die(`${flag} file is not valid JSON: ${sidecarPath}`, 1);
|
|
566
|
+
}
|
|
567
|
+
if (!Array.isArray(parsed)) {
|
|
568
|
+
const t = parsed === null ? 'null' : typeof parsed;
|
|
569
|
+
die(
|
|
570
|
+
`${flag} file must be a JSON array of {id, files} records, got type: ${t} — an object map would silently swallow a duplicate agent id`,
|
|
571
|
+
1,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
575
|
+
const rec = parsed[i];
|
|
576
|
+
if (rec === null || typeof rec !== 'object' || Array.isArray(rec)) {
|
|
577
|
+
die(`${flag} file entry #${i} must be an object with a "files" array`, 1);
|
|
578
|
+
}
|
|
579
|
+
if (!Array.isArray(rec.files) || !rec.files.every((f) => typeof f === 'string')) {
|
|
580
|
+
die(`${flag} file entry #${i} ("${rec.id ?? '<unnamed>'}") must have a "files" string array`, 1);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return parsed;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The repo's tracked files, for {@link findScopeCollisions}' glob∩glob witness
|
|
588
|
+
* stage. Spawned HERE and injected as a parameter because
|
|
589
|
+
* `scripts/lib/scope-gate.mjs` is hook-safe (pure, sync, no I/O, no spawn) and
|
|
590
|
+
* `hooks/enforce-scope.mjs` reaches it on a hot path.
|
|
591
|
+
*
|
|
592
|
+
* An unavailable git (not a repo, git missing, huge output) is NOT an error:
|
|
593
|
+
* the library documents `knownFiles` as optional — stage 3a simply has fewer
|
|
594
|
+
* witnesses and the prefix fallback of stage 3b carries the load. Silent by
|
|
595
|
+
* design: a WARN here would print on the success path of a mode whose contract
|
|
596
|
+
* is "quiet when clean".
|
|
597
|
+
*
|
|
598
|
+
* @returns {string[]}
|
|
599
|
+
*/
|
|
600
|
+
function knownRepoFiles() {
|
|
601
|
+
const opts = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024 };
|
|
602
|
+
try {
|
|
603
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], opts).trim();
|
|
604
|
+
if (!root) return [];
|
|
605
|
+
// -z: NUL-separated. Without it git QUOTES paths containing non-ASCII or
|
|
606
|
+
// special characters, and a quoted path would never match a scope entry.
|
|
607
|
+
return execFileSync('git', ['ls-files', '-z'], { ...opts, cwd: root })
|
|
608
|
+
.split('\0')
|
|
609
|
+
.filter((f) => f.length > 0);
|
|
610
|
+
} catch {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Assert that no file is claimed by two agents of the SAME wave (#1020).
|
|
617
|
+
* Exits 1 with one message per collision (plus one per duplicate agent id);
|
|
618
|
+
* returns silently when the wave is clean.
|
|
619
|
+
*
|
|
620
|
+
* @param {string} sidecarPath
|
|
621
|
+
*/
|
|
622
|
+
function assertDisjointOrDie(sidecarPath) {
|
|
623
|
+
const agentScopes = readAgentScopesOrDie(sidecarPath, '--assert-disjoint');
|
|
624
|
+
const { ok, collisions, duplicateIds } = findScopeCollisions(agentScopes, {
|
|
625
|
+
knownFiles: knownRepoFiles(),
|
|
626
|
+
});
|
|
627
|
+
if (ok) return;
|
|
628
|
+
|
|
629
|
+
// Duplicate ids FIRST: they are a malformed plan, and a reader who fixes them
|
|
630
|
+
// may well change which collisions remain.
|
|
631
|
+
for (const id of duplicateIds) {
|
|
632
|
+
process.stderr.write(
|
|
633
|
+
`ERROR: duplicate agent id in ${sidecarPath}: "${id}" — ids must be unique per wave; a copy-paste duplicate hides one agent's scope from every per-agent check\n`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
for (const c of collisions) {
|
|
637
|
+
process.stderr.write(
|
|
638
|
+
`ERROR: wave scope collision (${c.kind}): agents "${c.a}" and "${c.b}" both claim [${c.evidence.join(', ')}]\n`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`ERROR: ${collisions.length} scope collision(s), ${duplicateIds.length} duplicate id(s) — every file must belong to exactly ONE agent per wave (#1020; .claude/rules/parallel-sessions.md § Decision Tree)\n`,
|
|
643
|
+
);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* QUERY MODE (#1020): print `expandTestSiblings(unionFileScopes(scopes), {role})`
|
|
649
|
+
* as JSON on stdout, using the MANIFEST'S own role. Replaces the manifest echo —
|
|
650
|
+
* see the STDOUT CONTRACT note in the file header.
|
|
651
|
+
*
|
|
652
|
+
* @param {Record<string, unknown>} obj - the already schema-validated wave-scope object
|
|
653
|
+
* @param {string} sidecarPath
|
|
654
|
+
*/
|
|
655
|
+
function emitUnion(obj, sidecarPath) {
|
|
656
|
+
const agentScopes = readAgentScopesOrDie(sidecarPath, '--union');
|
|
657
|
+
const allowedPaths = expandScopeTestSiblings(unionFileScopes(agentScopes), { role: obj.role });
|
|
658
|
+
process.stdout.write(`${JSON.stringify(allowedPaths, null, 2)}\n`);
|
|
659
|
+
}
|
|
660
|
+
|
|
417
661
|
/**
|
|
418
662
|
* Main validation entry point. Reads input, validates, exits with appropriate code.
|
|
419
663
|
* @param {string} input - raw JSON string
|
|
420
664
|
* @param {string|null} [assertSubsetPath] - optional agent fileScope file for the #796 subset assertion
|
|
421
665
|
* @param {boolean} [expandTestSiblings] - opt-in #970 test-sibling coverage assertion
|
|
666
|
+
* @param {string|null} [assertDisjointPath] - optional per-agent scope sidecar for the #1020 collision check
|
|
667
|
+
* @param {string|null} [unionPath] - optional per-agent scope sidecar for the #1020 union query mode
|
|
422
668
|
*/
|
|
423
|
-
function validate(
|
|
669
|
+
function validate(
|
|
670
|
+
input,
|
|
671
|
+
assertSubsetPath = null,
|
|
672
|
+
expandTestSiblings = false,
|
|
673
|
+
assertDisjointPath = null,
|
|
674
|
+
unionPath = null,
|
|
675
|
+
) {
|
|
424
676
|
const obj = parseJson(input);
|
|
425
677
|
const errors = [];
|
|
426
678
|
const warnings = [];
|
|
@@ -446,9 +698,26 @@ function validate(input, assertSubsetPath = null, expandTestSiblings = false) {
|
|
|
446
698
|
assertSubsetOrDie(obj, assertSubsetPath, expandTestSiblings);
|
|
447
699
|
}
|
|
448
700
|
|
|
701
|
+
// #1020 — collision check runs AFTER the #796/#970 assertions, for the same
|
|
702
|
+
// reason #970 runs after #796 (see assertSubsetOrDie): a manifest that
|
|
703
|
+
// violates BOTH the subset relation and disjointness must keep the older,
|
|
704
|
+
// byte-pinned subset message. Only ever ADDS a failure mode.
|
|
705
|
+
if (assertDisjointPath) {
|
|
706
|
+
assertDisjointOrDie(assertDisjointPath);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// #1020 QUERY MODE — replaces the echo below; see the STDOUT CONTRACT note in
|
|
710
|
+
// the file header. Last, so every assertion above still gates it.
|
|
711
|
+
if (unionPath) {
|
|
712
|
+
emitUnion(obj, unionPath);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
449
716
|
// Echo validated JSON to stdout (trailing newline normalised)
|
|
450
717
|
process.stdout.write(input.endsWith('\n') ? input : input + '\n');
|
|
451
718
|
}
|
|
452
719
|
|
|
453
|
-
const { assertSubset, expandTestSiblings, positionals } = parseArgs(
|
|
454
|
-
|
|
720
|
+
const { assertSubset, expandTestSiblings, assertDisjoint, union, positionals } = parseArgs(
|
|
721
|
+
process.argv,
|
|
722
|
+
);
|
|
723
|
+
validate(readInput(positionals[0]), assertSubset, expandTestSiblings, assertDisjoint, union);
|
package/scripts/vault-mirror.mjs
CHANGED
|
@@ -55,7 +55,8 @@ import { createInterface } from 'node:readline';
|
|
|
55
55
|
import { createReadStream } from 'node:fs';
|
|
56
56
|
import { pathToFileURL } from 'node:url';
|
|
57
57
|
|
|
58
|
-
import { processLearning, processSession } from './lib/vault-mirror/process.mjs';
|
|
58
|
+
import { processLearning, processSession, getMaskerStats } from './lib/vault-mirror/process.mjs';
|
|
59
|
+
import { emitEvent } from './lib/events.mjs';
|
|
59
60
|
import { autoCommitVaultMirror } from './lib/vault-mirror/auto-commit.mjs';
|
|
60
61
|
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
61
62
|
import { resolveRepoNamespace } from './lib/vault-mirror/namespace.mjs';
|
|
@@ -403,6 +404,30 @@ async function main() {
|
|
|
403
404
|
}
|
|
404
405
|
}
|
|
405
406
|
|
|
407
|
+
// ── Masking telemetry (#1025) ───────────────────────────────────────────────
|
|
408
|
+
//
|
|
409
|
+
// Emitted UNCONDITIONALLY, exactly once per channel run, and HERE — at the end
|
|
410
|
+
// of the run rather than at the lazy build site inside process.mjs. The build
|
|
411
|
+
// site is only reached once a record is actually processed, so a run over an
|
|
412
|
+
// empty/fully-skipped source would emit nothing and "the masker never ran" would
|
|
413
|
+
// be indistinguishable from "this channel has no masker wired". Placed BEFORE
|
|
414
|
+
// the --strict-schema abort so a failing run still reports its masking posture.
|
|
415
|
+
//
|
|
416
|
+
// Counts only — never a needle, never a prefix of one, never masked text.
|
|
417
|
+
// Best-effort: a telemetry write must never be the reason a mirror run fails.
|
|
418
|
+
try {
|
|
419
|
+
const maskerStats = getMaskerStats();
|
|
420
|
+
await emitEvent('orchestrator.secret_masker.applied', {
|
|
421
|
+
channel: 'vault-mirror',
|
|
422
|
+
needle_count: maskerStats.needleCount,
|
|
423
|
+
records: maskerStats.records,
|
|
424
|
+
hits: maskerStats.hits,
|
|
425
|
+
dry_run: dryRun,
|
|
426
|
+
});
|
|
427
|
+
} catch {
|
|
428
|
+
// Silent no-op — see the note above.
|
|
429
|
+
}
|
|
430
|
+
|
|
406
431
|
// --strict-schema: abort with exit 1 when any entry was skipped-invalid.
|
|
407
432
|
// Useful in CI to catch producer-side schema drift early (issue #249).
|
|
408
433
|
if (strictSchema && skippedInvalidCount > 0) {
|
|
@@ -36,11 +36,11 @@ the GitHub mirror's main-branch SHA so the operator can confirm parity.
|
|
|
36
36
|
```bash
|
|
37
37
|
prev=""
|
|
38
38
|
while true; do
|
|
39
|
-
s=$(glab ci status
|
|
40
|
-
cur=$(jq -r '.[] | select(.status!="running" and .status!="pending") | "\(.name): \(.status)"' <<<"$s" 2>/dev/null | sort)
|
|
39
|
+
s=$(glab ci status -R <OWNER>/<REPO> --output json 2>/dev/null || echo '{"jobs":[]}')
|
|
40
|
+
cur=$(jq -r '.jobs[] | select(.status!="running" and .status!="pending") | "\(.name): \(.status)"' <<<"$s" 2>/dev/null | sort)
|
|
41
41
|
comm -13 <(echo "$prev") <(echo "$cur")
|
|
42
42
|
prev=$cur
|
|
43
|
-
jq -e 'all(.status=="success" or .status=="failed" or .status=="canceled" or .status=="skipped")' <<<"$s" >/dev/null 2>&1 && {
|
|
43
|
+
jq -e '(.jobs|length) > 0 and (.jobs | all(.status=="success" or .status=="failed" or .status=="canceled" or .status=="skipped"))' <<<"$s" >/dev/null 2>&1 && {
|
|
44
44
|
sha=$(gh api repos/Kanevry/session-orchestrator/commits/main --jq '.sha' 2>/dev/null || echo "(mirror unreachable)")
|
|
45
45
|
echo "GitHub mirror main: $sha"
|
|
46
46
|
break
|
|
@@ -55,8 +55,28 @@ done
|
|
|
55
55
|
SHA — silence at the end means glab JSON parsing failed (the `||` fallbacks
|
|
56
56
|
prevent the whole loop from dying).
|
|
57
57
|
|
|
58
|
+
**Probed 2026-08-14 (glab 1.91.0), three corrections — #1022.** The snippet
|
|
59
|
+
above was silence-is-not-success in its own right until that date, and each
|
|
60
|
+
half failed into the next one's fallback:
|
|
61
|
+
|
|
62
|
+
- `--pipeline-id LATEST` is not a glab flag on any `ci` subcommand
|
|
63
|
+
(`ERROR Unknown flag`, exit 1), so `$s` was always the `||` fallback. There is
|
|
64
|
+
no replacement — the argument-less form already selects the current branch's
|
|
65
|
+
pipeline. Avoid `--branch=<name>` here: it pins a snapshot taken when the
|
|
66
|
+
monitor was armed.
|
|
67
|
+
- The payload is an OBJECT (`{"jobs":[…],"pipeline":{…}}`), so `.[]` raised
|
|
68
|
+
`Cannot index array with string "status"` (jq exit 5) on every real response.
|
|
69
|
+
The accessor is `.jobs[]`.
|
|
70
|
+
- `all(…)` over the empty fallback returns **true**, so the loop broke and
|
|
71
|
+
printed the mirror SHA on its FIRST iteration whenever glab hiccuped — a
|
|
72
|
+
transient network error read as "pipeline finished". The `(.jobs|length) > 0`
|
|
73
|
+
guard is what makes the fallback non-terminal; verified by running the
|
|
74
|
+
terminal test against `{"jobs":[]}` (exit 1 = keep watching) versus the old
|
|
75
|
+
form against `[]` (exit 0 = break).
|
|
76
|
+
|
|
58
77
|
**GitHub-mirror equivalent.** When the pipeline is GitHub-Actions-native (PR
|
|
59
|
-
checks rather than a GitLab pipeline), use
|
|
78
|
+
checks rather than a GitLab pipeline), use
|
|
79
|
+
`gh pr checks -R <OWNER>/<REPO> <pr> --watch --fail-fast`
|
|
60
80
|
as the `command` source — it streams each check transition and exits non-zero on
|
|
61
81
|
the first failure, so the terminal state is never silent.
|
|
62
82
|
|
|
@@ -219,3 +219,20 @@ Net: `pid` (field notes above) stays forensic-only; `last_heartbeat` freshness i
|
|
|
219
219
|
### Schema v1 → v2 backward-compat
|
|
220
220
|
|
|
221
221
|
Readers (e.g., `readLock()` in `session-lock.mjs`, `discoverActiveSessions()`) MUST tolerate absent `last_heartbeat` and `semantic_session_id` fields (v1 locks written before Epic #583). When `last_heartbeat` is absent, fall back to TTL-based expiry from `started_at`. When `semantic_session_id` is absent, treat as unknown.
|
|
222
|
+
|
|
223
|
+
#### Schema v1 Sunset — evaluated 2026-08-15, tolerance RETAINED (#595)
|
|
224
|
+
|
|
225
|
+
The 90-day sunset window from Epic #583 (target 2026-08-25) came due and the removal was evaluated against the live fleet. **Verdict: keep the three reader tolerances; the blocker is not v1 data, it is a second production copy of the rule.**
|
|
226
|
+
|
|
227
|
+
**Precondition — zero v1 artefacts on disk (measured 2026-08-15, this host):**
|
|
228
|
+
|
|
229
|
+
- `find ~/Projects ~/.claude ~/.config /tmp/claude-501 -name 'session.lock' -not -path '*/node_modules/*'` → **12 files, 12/12 carry a non-empty `last_heartbeat`** (0 v1).
|
|
230
|
+
- `~/.config/session-orchestrator/sessions/active/*.json` → **3 entries, 3/3 carry the `mode` key** (0 v1).
|
|
231
|
+
- The only co-installed older plugin build (`~/.claude/plugins/cache/session-orchestrator/session-orchestrator/3.13.0`) already writes `last_heartbeat` (`session-lock.mjs:195`) and `mode` (`session-registry.mjs:209`) — **no v1 writer remains on this host.**
|
|
232
|
+
|
|
233
|
+
**Why the branches stay anyway:**
|
|
234
|
+
|
|
235
|
+
1. **`parseLock()` / `isLockLive()` — the rule is duplicated.** `scripts/lib/harness-audit/categories/category4.mjs` `lockIsLive()` inlines the same `last_heartbeat ?? started_at` fallback, and `tests/lib/lock-ttl-parity.test.mjs` asserts the mirror and the SSOT return identical verdicts *for a v1 lock*. Dropping it in `session-lock.mjs` alone breaks that parity by construction. A measured removal attempt turned **18 tests red across 4 files** (`session-discovery` 9, `session-discovery-fallback` 6, `lock-ttl-parity` 1, `on-session-start` 2) — all outside the lock/registry module pair, all seeding v1-shaped fixtures.
|
|
236
|
+
2. **`_validEntry()` optional `mode` — removal is a net safety LOSS.** Rejecting a mode-less registry entry drops a **live peer** from `readRegistry()`, making it invisible to the exclusivity matrix. An absent `mode` already degrades to the `parallel-ok` bucket, so strictening buys no detection and costs peer visibility — the wrong direction under `.claude/rules/development.md` § Guard & Threshold Design.
|
|
237
|
+
|
|
238
|
+
**What a real sunset needs (co-change set, one atomic MR):** `scripts/lib/session-lock.mjs` + `scripts/lib/harness-audit/categories/category4.mjs` (the mirror) + fixture updates in `tests/lib/session-discovery.test.mjs`, `tests/lib/session-discovery-fallback.test.mjs`, `tests/lib/lock-ttl-parity.test.mjs`, `tests/hooks/on-session-start.test.mjs`. Deleting the *mirror* in favour of importing the SSOT is the durable fix — the duplication, not the v1 data, is what keeps this class alive. The registry item should be closed as won't-do per point 2.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
You are the Design Facilitator — a thinking partner who shapes vague feature ideas into approved designs through Socratic questioning. You don't implement; you clarify. You don't collect wishes; you narrow the design space until one path is clearly better than the others.
|
|
6
6
|
|
|
7
|
-
You
|
|
7
|
+
You answer in the operator's language: `owner.language` in `~/.config/session-orchestrator/owner.yaml`, falling back to `en` when that file is missing, unreadable, or the key is absent — and following the operator's own language the moment he writes in another one. You meet people at their abstraction level — product language with stakeholders, technical language with engineers.
|
|
8
8
|
|
|
9
9
|
## Communication Principles
|
|
10
10
|
|
|
@@ -23,6 +23,52 @@ Three well-chosen AUQ rounds beat five meandering ones. When you have enough sig
|
|
|
23
23
|
### Confirm understanding before advancing
|
|
24
24
|
After Phase 1, summarize your understanding in 1-2 plain-text sentences before running the first dialogue round. After Phase 2, surface the running summary between rounds. Catching a misunderstanding at round 2 costs one turn; catching it at Phase 4 costs a rewrite.
|
|
25
25
|
|
|
26
|
+
### Length is a dial, not taste
|
|
27
|
+
How much you say around each question is set by the operator, not chosen by mood — see § Output Levels. "Less is more" is the discipline; the budget is the number that makes it checkable.
|
|
28
|
+
|
|
29
|
+
## Output Levels
|
|
30
|
+
|
|
31
|
+
The active level is `efficiency.output-level` in `~/.config/session-orchestrator/owner.yaml`. If that file is missing, unreadable, or the key is absent, the level is `full`. Apply the matching block below for the whole dialogue.
|
|
32
|
+
|
|
33
|
+
**How to read a budget.** A *round* is every chat line you author between one answered question and your next `AskUserQuestion` call — the running summary, the trade-off framing, the one sentence of recommendation reasoning. A budget is a ceiling, not a target: under is fine, over is a defect. You meet it by WITHHOLDING, never by dropping — no trade-off disappears, it moves into the option description where the operator can act on it.
|
|
34
|
+
|
|
35
|
+
**The spec carries no budget.** The file you write to `docs/specs/` is the deliverable, not the conversation. Budgets bound chat only; the spec keeps its full Out-of-Scope and Open-Questions sections, and pointing at it is the preferred way to stay under one.
|
|
36
|
+
|
|
37
|
+
**Escalation (all levels).** When the operator writes `expand <topic>` (German: `mehr zu <Abschnitt>`), print that topic's full detail immediately, without re-asking and without the budget applying to that one response.
|
|
38
|
+
|
|
39
|
+
**Never traded for brevity (all levels).** No budget may be met by cutting any of the following. Where a budget and one of them collide, the budget yields:
|
|
40
|
+
- input validation, and the reporting of invalid input;
|
|
41
|
+
- error handling, error messages, and failure disclosure — a swallowed error is never "concise";
|
|
42
|
+
- security findings, warnings, and destructive-action confirmations (PSA-003);
|
|
43
|
+
- accessibility of the output itself — no meaning carried by colour or emoji alone, no bare unlabelled numbers, no table whose header you dropped to save a line;
|
|
44
|
+
- anything the operator explicitly asked to see;
|
|
45
|
+
- the concrete pro and the concrete con on each option (`.claude/rules/ask-via-tool.md` AUQ-002/AUQ-003) — a budget never buys back the menu this skill exists to avoid.
|
|
46
|
+
|
|
47
|
+
### output-level: ultra
|
|
48
|
+
- Meaning: telegraphic — the running summary, the trade-offs, the question. No narration.
|
|
49
|
+
- Budget: ≤10 lines per round; ≤2 lines of running summary; ≤1 line of recommendation reasoning before the tool call.
|
|
50
|
+
- Shape: bullets only. Trade-offs as `<option> — gains <X>, costs <Y>`. Never restate the user's last answer back at them.
|
|
51
|
+
- Escalation: `expand <topic>` — see § Escalation above.
|
|
52
|
+
|
|
53
|
+
### output-level: full
|
|
54
|
+
- Meaning: terse but complete — framing trimmed, trade-offs preserved. This is the default.
|
|
55
|
+
- Budget: ≤25 lines per round; ≤4 lines of running summary; ≤2 lines of recommendation reasoning before the tool call.
|
|
56
|
+
- Shape: name the design tension in one line, then the options. Prose only where a bullet would lose the causal link between a choice and what it forecloses.
|
|
57
|
+
- Escalation: `expand <topic>` — see § Escalation above.
|
|
58
|
+
|
|
59
|
+
### output-level: lite
|
|
60
|
+
- Meaning: verbose — the reasoning behind the narrowing is spelled out. Chosen for learning, not for speed.
|
|
61
|
+
- Budget: ≤60 lines per round; ≤10 lines of running summary. Still a ceiling — `lite` is not "unbounded".
|
|
62
|
+
- Shape: explain WHY each option is on the table, name the paths you already discarded and why, define unfamiliar terms on first use.
|
|
63
|
+
- Escalation: `expand <topic>` — see § Escalation above.
|
|
64
|
+
|
|
65
|
+
### Companion dials
|
|
66
|
+
|
|
67
|
+
Same file, same lookup, same fallback-to-default rule:
|
|
68
|
+
|
|
69
|
+
- `efficiency.preamble` — `minimal` (default): at most one clause before a tool call, and only when the next step is non-obvious; never "Let me check the repo." immediately followed by checking it. `verbose`: one sentence before each tool call naming what you expect to find.
|
|
70
|
+
- `tone.style` — `direct` (this soul's baseline: name a confused framing plainly and reframe), `neutral` (state the tension without advocacy; still recommend when asked), `friendly` (same content, softer framing; never softer facts).
|
|
71
|
+
|
|
26
72
|
## Decision-Making Philosophy
|
|
27
73
|
|
|
28
74
|
When design ambiguity arises, resolve it in this order:
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-md-drift-check
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use when detecting drift between CLAUDE.md (or AGENTS.md, the Codex CLI alias) / _meta narrative and
|
|
5
|
+
live repository state. Ten checks: absolute-path resolution, 01-projects/ count claims, issue-reference
|
|
6
|
+
freshness, session-file existence, command-count sync, session-config-parity (mandatory template keys =
|
|
7
|
+
error, opt-in gaps = warning), vault-dir-parity (CLAUDE.md vs AGENTS.md), generated-rule-staleness
|
|
8
|
+
(WARN-only), rule-scoping (paths:/globs: frontmatter defects, dangling rule citations, zero-match
|
|
9
|
+
globs), and docs-parity (docs/components.md count-claims vs on-disk counts, template-vs-reference
|
|
10
|
+
config-key parity, stale .claude/metrics/ paths). Full per-check spec in the body table. Invoked as an
|
|
11
|
+
opt-in session-end phase; mirrors vault-sync's lean JSON+exit-code contract.
|
|
4
12
|
model: haiku
|
|
5
13
|
---
|
|
6
14
|
|
package/skills/debug/SKILL.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use when encountering any bug, test failure, build break, or unexpected behavior — runs a 4-phase
|
|
5
|
+
systematic debugging process before proposing any fix. Iron Law: NO FIXES WITHOUT ROOT CAUSE
|
|
6
|
+
INVESTIGATION FIRST. Produces a `.orchestrator/debug/` artifact the fixer agent must reference.
|
|
4
7
|
model: inherit
|
|
5
8
|
color: red
|
|
6
9
|
tools: Read, Grep, Glob, Bash, Write
|
|
@@ -79,7 +79,7 @@ Used for a single finding from a single probe.
|
|
|
79
79
|
**CLI Example:**
|
|
80
80
|
```bash
|
|
81
81
|
# GitHub
|
|
82
|
-
gh issue create \
|
|
82
|
+
gh issue create -R <OWNER>/<REPO> \
|
|
83
83
|
--title "[Discovery] <finding_title>" \
|
|
84
84
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
85
85
|
--body "$(cat <<'EOF'
|
|
@@ -88,7 +88,7 @@ EOF
|
|
|
88
88
|
)"
|
|
89
89
|
|
|
90
90
|
# GitLab
|
|
91
|
-
glab issue create \
|
|
91
|
+
glab issue create -R <OWNER>/<REPO> \
|
|
92
92
|
--title "[Discovery] <finding_title>" \
|
|
93
93
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
94
94
|
--description "$(cat <<'EOF'
|
|
@@ -159,7 +159,7 @@ Used when multiple related findings from the same probe or category are grouped
|
|
|
159
159
|
**CLI Example:**
|
|
160
160
|
```bash
|
|
161
161
|
# GitHub
|
|
162
|
-
gh issue create \
|
|
162
|
+
gh issue create -R <OWNER>/<REPO> \
|
|
163
163
|
--title "[Discovery] <category> audit: <summary>" \
|
|
164
164
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
165
165
|
--body "$(cat <<'EOF'
|
|
@@ -168,7 +168,7 @@ EOF
|
|
|
168
168
|
)"
|
|
169
169
|
|
|
170
170
|
# GitLab
|
|
171
|
-
glab issue create \
|
|
171
|
+
glab issue create -R <OWNER>/<REPO> \
|
|
172
172
|
--title "[Discovery] <category> audit: <summary>" \
|
|
173
173
|
--label "type:discovery,priority::<level>,area:<area>,status:ready" \
|
|
174
174
|
--description "$(cat <<'EOF'
|
|
@@ -58,9 +58,9 @@ Grep pattern: (TODO|FIXME|HACK|XXX|TEMP|WORKAROUND)[\s:()\-]
|
|
|
58
58
|
For each match, check whether a corresponding VCS issue exists:
|
|
59
59
|
```bash
|
|
60
60
|
# Search for issue referencing the annotation text
|
|
61
|
-
gh issue list --search "<annotation text>" --limit 5
|
|
61
|
+
gh issue list -R <OWNER>/<REPO> --search "<annotation text>" --limit 5
|
|
62
62
|
# or
|
|
63
|
-
glab issue list --search "<annotation text>" --per-page 5
|
|
63
|
+
glab issue list -R <OWNER>/<REPO> --search "<annotation text>" --per-page 5
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Flag annotations with no corresponding issue.
|
|
@@ -113,11 +113,11 @@ Export Status: exported/public | internal
|
|
|
113
113
|
1. List open feature/enhancement issues via the VCS CLI (syntax reference: `skills/gitlab-ops/SKILL.md` § "Common CLI Commands" — do not duplicate CLI flags here beyond what's needed to name the call):
|
|
114
114
|
```bash
|
|
115
115
|
# GitLab
|
|
116
|
-
glab issue list --label "feature" --per-page 100
|
|
117
|
-
glab issue list --label "enhancement" --per-page 100
|
|
116
|
+
glab issue list -R <OWNER>/<REPO> --label "feature" --per-page 100
|
|
117
|
+
glab issue list -R <OWNER>/<REPO> --label "enhancement" --per-page 100
|
|
118
118
|
# GitHub
|
|
119
|
-
gh issue list --label "feature" --limit 100
|
|
120
|
-
gh issue list --label "enhancement" --limit 100
|
|
119
|
+
gh issue list -R <OWNER>/<REPO> --label "feature" --limit 100
|
|
120
|
+
gh issue list -R <OWNER>/<REPO> --label "enhancement" --limit 100
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
2. Extract theme keywords from each issue's title (and first paragraph of body where available): lowercase, strip stop-words, tokenize on non-alphanumeric boundaries. Group issues that share two or more significant keywords (e.g. "export", "csv", "invoice") into a candidate cluster.
|
|
@@ -125,10 +125,10 @@ gh issue list --label "enhancement" --limit 100
|
|
|
125
125
|
3. For each candidate cluster with 3 or more issues, check whether ANY member issue is already linked to an epic (`epic` label, a GitLab Epic relationship, or an in-body reference such as `part of #<epic-iid>` / `Epic: #<epic-iid>`):
|
|
126
126
|
```bash
|
|
127
127
|
# GitLab -- inspect labels + description for epic linkage
|
|
128
|
-
glab issue view <IID>
|
|
128
|
+
glab issue view -R <OWNER>/<REPO> <IID>
|
|
129
129
|
Grep pattern: (^Labels:.*\bepic\b|part of #|Epic:\s*#)
|
|
130
130
|
# GitHub -- same check against issue body/labels
|
|
131
|
-
gh issue view <NUMBER>
|
|
131
|
+
gh issue view -R <OWNER>/<REPO> <NUMBER>
|
|
132
132
|
Grep pattern: (^labels:.*\bepic\b|part of #|Epic:\s*#)
|
|
133
133
|
```
|
|
134
134
|
|