session-orchestrator 4.2.0 → 5.0.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/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +94 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/commands/ux-grill.md +51 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/codex-setup.md +8 -0
- package/docs/components.md +7 -7
- package/docs/events-schema.md +9 -5
- package/docs/github-mirror-protection.md +61 -20
- package/docs/migration-v5.md +62 -0
- package/docs/scope-collision-guard.md +14 -0
- package/hooks/_lib/hook-import-set.json +44 -2
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +190 -3
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +51 -4
- package/package.json +2 -1
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +114 -13
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget.mjs +336 -6
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +11 -1
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +14 -65
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-start-probes.mjs +419 -53
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +48 -20
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/mcp-server.sh +16 -1
- package/scripts/release.mjs +7 -2
- package/skills/bootstrap/SKILL.md +12 -209
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +7 -3
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +10 -0
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +4 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-plan/SKILL.md +2 -144
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-start/SKILL.md +24 -6
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +3 -128
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +18 -5
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -629,28 +629,42 @@ function importClosure(repoRoot, entry, readSource) {
|
|
|
629
629
|
*/
|
|
630
630
|
function checkIgnoreBatch(repoRoot, specs, out) {
|
|
631
631
|
if (specs.length === 0) return;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
632
|
+
let pending = specs;
|
|
633
|
+
for (;;) {
|
|
634
|
+
const ci = spawnSync('git', ['check-ignore', '--stdin'], {
|
|
635
|
+
cwd: repoRoot, input: pending.join('\n'), encoding: 'utf8',
|
|
636
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
637
|
+
});
|
|
638
|
+
// 0 = at least one ignored (listed on stdout); 1 = none ignored; else fatal.
|
|
639
|
+
if (ci.status === 0) {
|
|
640
|
+
for (const line of (ci.stdout || '').split('\n')) {
|
|
641
|
+
const p = line.trim();
|
|
642
|
+
if (p) out.add(p);
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
641
645
|
}
|
|
646
|
+
if (ci.status === 1) return;
|
|
647
|
+
// Git identifies this refused candidate precisely. Retrying only the rest
|
|
648
|
+
// preserves the old dropped-candidate semantics without bisecting healthy
|
|
649
|
+
// paths (78 subprocesses for five symlink descendants in the tracked tree).
|
|
650
|
+
const rejected = /^fatal: pathspec '([^'\n]+)' is beyond a symbolic link\r?\n?$/.exec(ci.stderr || '')?.[1];
|
|
651
|
+
if (rejected && pending.includes(rejected)) {
|
|
652
|
+
process.stderr.write(` WARN: git check-ignore rejected "${rejected}" — candidate dropped (${(ci.stderr || '').trim()})\n`);
|
|
653
|
+
pending = pending.filter((spec) => spec !== rejected);
|
|
654
|
+
if (pending.length === 0) return;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (pending.length === 1) {
|
|
658
|
+
process.stderr.write(
|
|
659
|
+
` WARN: git check-ignore rejected "${pending[0]}" — candidate dropped (${(ci.stderr || '').trim().split('\n')[0]})\n`,
|
|
660
|
+
);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const mid = pending.length >> 1;
|
|
664
|
+
checkIgnoreBatch(repoRoot, pending.slice(0, mid), out);
|
|
665
|
+
checkIgnoreBatch(repoRoot, pending.slice(mid), out);
|
|
642
666
|
return;
|
|
643
667
|
}
|
|
644
|
-
if (ci.status === 1) return;
|
|
645
|
-
if (specs.length === 1) {
|
|
646
|
-
process.stderr.write(
|
|
647
|
-
` WARN: git check-ignore rejected "${specs[0]}" — candidate dropped (${(ci.stderr || '').trim().split('\n')[0]})\n`,
|
|
648
|
-
);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
const mid = specs.length >> 1;
|
|
652
|
-
checkIgnoreBatch(repoRoot, specs.slice(0, mid), out);
|
|
653
|
-
checkIgnoreBatch(repoRoot, specs.slice(mid), out);
|
|
654
668
|
}
|
|
655
669
|
|
|
656
670
|
/**
|
|
@@ -795,9 +795,18 @@ function mentionedModuleTokens(lines) {
|
|
|
795
795
|
* switched off. Revisit if a real module-resolver (import-specifier resolution
|
|
796
796
|
* relative to the importing file) becomes cheap, or if a collided basename is
|
|
797
797
|
* ever confirmed to mask a true positive. The `coordinator-invoked-module`
|
|
798
|
-
* DOWNGRADE is exempt
|
|
799
|
-
* `dirname/base` suffix, because
|
|
800
|
-
* reportable class and would otherwise
|
|
798
|
+
* DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter: in both
|
|
799
|
+
* a colliding basename must be named with its `dirname/base` suffix, because
|
|
800
|
+
* those matches move a module OUT of the reportable class and would otherwise
|
|
801
|
+
* hide a true unreachable sibling. Measured cost of leaving the root filter
|
|
802
|
+
* on bare basenames: `locks/index.mjs` and `worktree/index.mjs` were both
|
|
803
|
+
* suppressed by a third unreachable module that merely mentioned bare
|
|
804
|
+
* `index.mjs`, and only resurfaced when that module was deleted for an
|
|
805
|
+
* unrelated reason. Remaining ceiling: two ambiguous roots whose mentioning
|
|
806
|
+
* module ALSO carries the qualified form (e.g. an unreachable module that
|
|
807
|
+
* literally writes `locks/index.mjs`) are still suppressed — correct when it
|
|
808
|
+
* is a real reference, a mask when it is prose. Revisit if a qualified
|
|
809
|
+
* mention is ever confirmed to hide a root.
|
|
801
810
|
* - **Reachable ≠ executed.** A module imported by a hook that never takes that
|
|
802
811
|
* branch reads as wired here. Proving execution needs coverage data, not a graph.
|
|
803
812
|
* - **Reachable from SOME entrypoint is not reachable from the PROMISED one.**
|
|
@@ -840,6 +849,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
840
849
|
// module an operator flagged. Measured 2026-08-28 on the first S4
|
|
841
850
|
// allowlist entry: 52 → 51 unreachable modules plus one bogus stale line.
|
|
842
851
|
mentions: relative === SELF_REL ? new Set() : mentionedModuleTokens(lines),
|
|
852
|
+
// Raw text, kept for the QUALIFIED (`dirname/base`) re-check in the
|
|
853
|
+
// root filter below: `mentionedModuleTokens` strips the directory, so
|
|
854
|
+
// a colliding basename can only be disambiguated against the body.
|
|
855
|
+
rawBody: relative === SELF_REL ? '' : body,
|
|
843
856
|
};
|
|
844
857
|
});
|
|
845
858
|
|
|
@@ -877,10 +890,38 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
877
890
|
(module) => !reachable.has(module.relative) && !module.entrypoint && module.exports.length > 0,
|
|
878
891
|
);
|
|
879
892
|
const unreachableSet = new Set(unreachable.map((module) => module.relative));
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
)
|
|
893
|
+
// Basename census, shared by the root filter below and the downgrade half
|
|
894
|
+
// further down. A bare basename is only a valid module reference when it is
|
|
895
|
+
// UNIQUE in the corpus: `writer.mjs` names both `peer-cards/writer.mjs` and
|
|
896
|
+
// `reconcile/writer.mjs` (measured 2026-09-07), so a doc naming ONE of them
|
|
897
|
+
// would otherwise downgrade BOTH out of the reportable class — a true
|
|
898
|
+
// unreachable silently moved into the advisory half. For a colliding basename
|
|
899
|
+
// the reference must therefore carry at least the `dirname/base` suffix
|
|
900
|
+
// (`reconcile/writer.mjs`); unique basenames keep the cheaper bare match.
|
|
901
|
+
// Direction matters in both consumers: this can only ever ADD findings back to
|
|
902
|
+
// the reportable class, never remove one.
|
|
903
|
+
/** @type {Map<string, number>} */
|
|
904
|
+
const basenameCount = new Map();
|
|
905
|
+
for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
|
|
906
|
+
|
|
907
|
+
const roots = unreachable.filter((module) => {
|
|
908
|
+
// A bare-basename mention only suppresses when the basename is UNIQUE
|
|
909
|
+
// (see the census above). When it collides, the mentioning module must name
|
|
910
|
+
// the `dirname/base` form in its body — otherwise ONE unreachable module
|
|
911
|
+
// mentioning bare `index.mjs` masks EVERY differently-pathed `index.mjs`
|
|
912
|
+
// root at once (#1293: `locks/index.mjs` + `worktree/index.mjs` were masked
|
|
913
|
+
// by a third unreachable module until that module was deleted for an
|
|
914
|
+
// unrelated reason).
|
|
915
|
+
const ambiguous = (basenameCount.get(module.base) ?? 0) > 1;
|
|
916
|
+
const qualified = module.relative.split(path.sep).slice(-2).join('/');
|
|
917
|
+
const qualifiedRe = ambiguous ? tokenMatcher(qualified) : null;
|
|
918
|
+
return !unreachable.some(
|
|
919
|
+
(other) =>
|
|
920
|
+
other.relative !== module.relative &&
|
|
921
|
+
other.mentions.has(module.base) &&
|
|
922
|
+
(qualifiedRe === null || qualifiedRe.test(other.rawBody)),
|
|
923
|
+
);
|
|
924
|
+
});
|
|
884
925
|
|
|
885
926
|
// Category split (see § Category split in the doc block above): an INSTRUCTION
|
|
886
927
|
// document that names both the module AND one of its exported symbols is an
|
|
@@ -894,19 +935,6 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
894
935
|
.sort()
|
|
895
936
|
.map((file) => ({ relative: path.relative(pluginRoot, file), body: readFileSync(file, 'utf8') }));
|
|
896
937
|
|
|
897
|
-
// Basename census for the downgrade half. A bare basename is only a valid
|
|
898
|
-
// module reference when it is UNIQUE in the corpus: `writer.mjs` names both
|
|
899
|
-
// `peer-cards/writer.mjs` and `reconcile/writer.mjs` (measured 2026-09-07),
|
|
900
|
-
// so a doc naming ONE of them would otherwise downgrade BOTH out of the
|
|
901
|
-
// reportable class — a true unreachable silently moved into the advisory
|
|
902
|
-
// half. For a colliding basename the doc must therefore carry at least the
|
|
903
|
-
// `dirname/base` suffix (`reconcile/writer.mjs`); unique basenames keep the
|
|
904
|
-
// cheaper bare match. Direction matters: this can only ever ADD findings back
|
|
905
|
-
// to the reportable class, never remove one.
|
|
906
|
-
/** @type {Map<string, number>} */
|
|
907
|
-
const basenameCount = new Map();
|
|
908
|
-
for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
|
|
909
|
-
|
|
910
938
|
let coordinatorInvoked = 0;
|
|
911
939
|
const findings = roots.map((module) => {
|
|
912
940
|
// Docs write POSIX separators regardless of host; `path.relative` does not.
|
|
@@ -60,6 +60,15 @@ const DEFAULT_POLL_MS = 50;
|
|
|
60
60
|
* REVISIT when a board sweep is measured above 30 s (half the TTL — the point
|
|
61
61
|
* at which a slow host crosses it), or when an `onLockOutcome` carrying
|
|
62
62
|
* `staleOverride` is observed in the events ledger on a host that had no crash.
|
|
63
|
+
*
|
|
64
|
+
* What expiry does NOT do: protect the section it bounds — past 60 s the next
|
|
65
|
+
* writer takes over while this one may still run (see file-lock.mjs § Lease
|
|
66
|
+
* semantics). What IS guaranteed since #1285: the late writer's release can no
|
|
67
|
+
* longer delete the successor's lock, because release runs under the same
|
|
68
|
+
* `.acquire` guard as takeover. A `not-owner` release here therefore means THIS
|
|
69
|
+
* writer's lease expired mid-section — the lost-update case above, never a
|
|
70
|
+
* benign miss. Since #1336 that release result reaches callers through
|
|
71
|
+
* `onReleaseOutcome` (see withBoardLock).
|
|
63
72
|
*/
|
|
64
73
|
const DEFAULT_STALE_MS = 60_000;
|
|
65
74
|
|
|
@@ -105,6 +114,11 @@ export function boardLockPathFor(vaultDir) {
|
|
|
105
114
|
* is present only when this acquire OVERRODE an aged lock, and carries
|
|
106
115
|
* `file-lock.mjs`'s own reason token — the observable behind the
|
|
107
116
|
* DEFAULT_STALE_MS revisit trigger.
|
|
117
|
+
* @param {(release: { ok: boolean, reason?: string }) => void} [opts.onReleaseOutcome]
|
|
118
|
+
* — diagnostic sink for the release result, called at most once, AFTER `fn`
|
|
119
|
+
* (only on the locked path). `{ ok: false, reason: 'not-owner' }` means this
|
|
120
|
+
* writer's lease expired mid-section; `'busy'` means the release gave up on
|
|
121
|
+
* the `.acquire` guard and left the lock in place.
|
|
108
122
|
* @param {(lockPath: string, fn: Function, opts: object) => Promise<object>} [opts.lockImpl]
|
|
109
123
|
* — test seam; defaults to {@link withFileLock}. Must honour the same
|
|
110
124
|
* `{ ok: true, value } | { ok: false, reason }` contract.
|
|
@@ -123,6 +137,7 @@ export async function withBoardLock(vaultDir, fn, opts = {}) {
|
|
|
123
137
|
staleMs = DEFAULT_STALE_MS,
|
|
124
138
|
holder: holderOpt,
|
|
125
139
|
onLockOutcome,
|
|
140
|
+
onReleaseOutcome,
|
|
126
141
|
lockImpl = withFileLock,
|
|
127
142
|
warn = (msg) => process.stderr.write(msg),
|
|
128
143
|
} = opts;
|
|
@@ -167,6 +182,9 @@ export async function withBoardLock(vaultDir, fn, opts = {}) {
|
|
|
167
182
|
indent: 2,
|
|
168
183
|
tmpPrefix: '.board.lock',
|
|
169
184
|
warn: warnAndWatch,
|
|
185
|
+
// Separate sink, not a second onLockOutcome call — that one stays
|
|
186
|
+
// "exactly once, before fn".
|
|
187
|
+
...(typeof onReleaseOutcome === 'function' ? { onRelease: onReleaseOutcome } : {}),
|
|
170
188
|
},
|
|
171
189
|
);
|
|
172
190
|
|
|
@@ -1095,6 +1095,14 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
1095
1095
|
waited_ms: Date.now() - acquireStartedAt,
|
|
1096
1096
|
};
|
|
1097
1097
|
},
|
|
1098
|
+
// #1336: the release runs in withFileLock's finally, i.e. before
|
|
1099
|
+
// withBoardLock returns — so this lands on the SAME board_written event,
|
|
1100
|
+
// which mirrorBoard emits only after this function returns.
|
|
1101
|
+
onReleaseOutcome: (r) => {
|
|
1102
|
+
if (lockOutcome && r && r.ok === false && typeof r.reason === 'string') {
|
|
1103
|
+
lockOutcome.release = r.reason;
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1098
1106
|
});
|
|
1099
1107
|
|
|
1100
1108
|
return lockOutcome === undefined ? inner : { ...inner, lock: lockOutcome };
|
package/scripts/mcp-server.sh
CHANGED
|
@@ -194,8 +194,20 @@ tool_session_metrics() {
|
|
|
194
194
|
local entries
|
|
195
195
|
entries=$(jq -R -c 'fromjson? | select(.status != "abandoned")' "$metrics_file" 2>/dev/null | tail -n 5) || true
|
|
196
196
|
|
|
197
|
+
# Report the stubs the filter above dropped (#1296) — counted over the WHOLE
|
|
198
|
+
# file, not the tail, with the same torn-line tolerance (`fromjson?`, plus
|
|
199
|
+
# `.status?` so a non-object line cannot abort the count either). Computed
|
|
200
|
+
# BEFORE the empty check: a stubs-only ledger is not an empty file.
|
|
201
|
+
local stub_count
|
|
202
|
+
stub_count=$(jq -R -n '[inputs | fromjson? | select(.status? == "abandoned")] | length' "$metrics_file" 2>/dev/null) || stub_count=0
|
|
203
|
+
[[ -z "$stub_count" ]] && stub_count=0
|
|
204
|
+
|
|
197
205
|
if [[ -z "$entries" ]]; then
|
|
198
|
-
|
|
206
|
+
if [[ "$stub_count" -gt 0 ]]; then
|
|
207
|
+
respond "$id" "$(text_content "No real sessions (abandoned stubs excluded: ${stub_count})")"
|
|
208
|
+
else
|
|
209
|
+
respond "$id" "$(text_content "No metrics found (file is empty)")"
|
|
210
|
+
fi
|
|
199
211
|
return
|
|
200
212
|
fi
|
|
201
213
|
|
|
@@ -229,6 +241,9 @@ tool_session_metrics() {
|
|
|
229
241
|
' 2>/dev/null) || token_summary=""
|
|
230
242
|
|
|
231
243
|
local output
|
|
244
|
+
entries="${entries}
|
|
245
|
+
|
|
246
|
+
abandoned stubs excluded: ${stub_count}"
|
|
232
247
|
if [[ -n "$token_summary" ]]; then
|
|
233
248
|
output="${entries}
|
|
234
249
|
|
package/scripts/release.mjs
CHANGED
|
@@ -378,8 +378,13 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
|
|
|
378
378
|
// (same 2026-09-07 sweep): site/llms-full.txt ("The v4.0.0 release REMOVES public surfaces" — its
|
|
379
379
|
// version SURFACE is checked separately by the SURFACES row, so the sweep on it is redundant),
|
|
380
380
|
// skills/architecture/references/domain-model.md ("Merged here in v4.0.0"), skills/autopilot/SKILL.md
|
|
381
|
-
// ("4.0.0 — see docs/migration-v4.md")
|
|
382
|
-
|
|
381
|
+
// ("4.0.0 — see docs/migration-v4.md"). A fifth, templates/_shared/journey-manifest.md ("Retired …
|
|
382
|
+
// in 4.0.0"), was DELETED on 2026-09-12 (replaced by ux-manifest.template.md, Epic #1322) and its
|
|
383
|
+
// allowlist entry went with it — re-add only if a file of that name returns.
|
|
384
|
+
// September 10 campaign snapshot: inputs and receipt reproduce the dated 4.3-planned
|
|
385
|
+
// render made while 4.2.0 was current. Exact files only; neighboring marketing
|
|
386
|
+
// remains checked. Do not bump receipt props without a newly reviewed render.
|
|
387
|
+
export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|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|commands\/release\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
|
|
383
388
|
|
|
384
389
|
/** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
|
|
385
390
|
export function checkLeakage(lines) {
|
|
@@ -180,136 +180,21 @@ Entered when `$ARGUMENTS` contains `--upgrade <tier>`. No scaffolding questions
|
|
|
180
180
|
|
|
181
181
|
## Retroactive Flow (`--retroactive`)
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
Adopts an existing repo that already has `CLAUDE.md`/`AGENTS.md` + `## Session Config` but no `bootstrap.lock` — infers tier from file inventory and patches missing mandatory Session Config fields with defaults.
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
See [references/bootstrap-retroactive-flow.md](references/bootstrap-retroactive-flow.md).
|
|
186
186
|
|
|
187
|
-
**
|
|
188
|
-
|
|
189
|
-
1. **Verify preconditions.** Confirm `CLAUDE.md` (or `AGENTS.md`) exists and contains `## Session Config`. If not, abort: `Error: CLAUDE.md with Session Config required for retroactive bootstrap.`
|
|
190
|
-
|
|
191
|
-
2. **Check lock not already present.** If `.orchestrator/bootstrap.lock` already exists and has valid `version` + `tier` fields, report: `bootstrap.lock already present (tier: <tier>). Nothing to do.` and exit 0 (idempotent).
|
|
192
|
-
|
|
193
|
-
3. **Infer tier from file inventory.** Examine the repo root:
|
|
194
|
-
|
|
195
|
-
| Condition (evaluated in order) | Inferred Tier |
|
|
196
|
-
|---|---|
|
|
197
|
-
| CI file present (`.gitlab-ci.yml` OR `.github/workflows/`) AND `CHANGELOG.md` present | `deep` |
|
|
198
|
-
| Package manifest present (`package.json` OR `pyproject.toml`) | `standard` |
|
|
199
|
-
| Neither of the above | `fast` |
|
|
200
|
-
|
|
201
|
-
Store as `INFERRED_TIER`.
|
|
202
|
-
|
|
203
|
-
4. **Infer archetype.** Run Phase 0.5's read-only source detection. For a private
|
|
204
|
-
contract, use its detected `selected.id`; retain `null` with an explicit
|
|
205
|
-
`insufficient-evidence` report if no markers match. An invalid configured
|
|
206
|
-
contract aborts. Do not scaffold or apply rules in this retroactive flow.
|
|
207
|
-
For the public path, use best-effort detection from existing files:
|
|
208
|
-
- `pyproject.toml` present → `python-uv`
|
|
209
|
-
- `package.json` with `next` in dependencies → `nextjs-minimal`
|
|
210
|
-
- `package.json` without `next` → `node-minimal`
|
|
211
|
-
- No manifest → `null`
|
|
212
|
-
|
|
213
|
-
Store as `INFERRED_ARCHETYPE`.
|
|
214
|
-
|
|
215
|
-
5. **Write bootstrap.lock.** Create `.orchestrator/` if needed, then write:
|
|
216
|
-
```yaml
|
|
217
|
-
# .orchestrator/bootstrap.lock
|
|
218
|
-
version: 1
|
|
219
|
-
tier: <INFERRED_TIER>
|
|
220
|
-
archetype: <INFERRED_ARCHETYPE or null>
|
|
221
|
-
timestamp: <current ISO 8601 UTC>
|
|
222
|
-
source: retroactive
|
|
223
|
-
plugin-version: <current plugin version from $PLUGIN_ROOT/package.json>
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
6. **Patch Session Config (#182).** Run the validator against the current `## Session Config` block; append any missing mandatory fields with defaults. The 7 mandatory fields (per `scripts/lib/config-schema.mjs`) are: `test-command`, `typecheck-command`, `lint-command`, `agents-per-wave`, `waves`, `persistence`, `enforcement`.
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
CONFIG_OUT="$(node "$PLUGIN_ROOT/scripts/parse-config.mjs" 2>&1 >/dev/null)"
|
|
230
|
-
# parse-config.mjs emits validation warnings to stderr when enforcement=warn.
|
|
231
|
-
# Grep for 'must be' lines (issued by validate-config.mjs) to detect missing fields.
|
|
232
|
-
MISSING_FIELDS="$(echo "$CONFIG_OUT" | grep -oE '(test-command|typecheck-command|lint-command|agents-per-wave|waves|persistence|enforcement)' | sort -u || true)"
|
|
233
|
-
if [[ -n "$MISSING_FIELDS" ]]; then
|
|
234
|
-
# Detect package manager to pick sensible defaults for commands.
|
|
235
|
-
PM_DEFAULTS="$(node --input-type=module -e "
|
|
236
|
-
import {detectPackageManager, defaultQualityGateCommands} from '$PLUGIN_ROOT/scripts/lib/package-manager.mjs';
|
|
237
|
-
const pm = detectPackageManager(process.cwd());
|
|
238
|
-
const cmds = defaultQualityGateCommands(pm);
|
|
239
|
-
console.log('test-command: ' + cmds.test.command);
|
|
240
|
-
console.log('typecheck-command: ' + cmds.typecheck.command);
|
|
241
|
-
console.log('lint-command: ' + cmds.lint.command);
|
|
242
|
-
" 2>/dev/null)"
|
|
243
|
-
|
|
244
|
-
CONFIG_FILE="CLAUDE.md"
|
|
245
|
-
[[ -f "AGENTS.md" ]] && CONFIG_FILE="AGENTS.md"
|
|
246
|
-
|
|
247
|
-
# Append each missing field under the ## Session Config block.
|
|
248
|
-
for field in $MISSING_FIELDS; do
|
|
249
|
-
case "$field" in
|
|
250
|
-
test-command|typecheck-command|lint-command)
|
|
251
|
-
default_line="$(echo "$PM_DEFAULTS" | grep "^$field:")" ;;
|
|
252
|
-
agents-per-wave) default_line="agents-per-wave: 6" ;;
|
|
253
|
-
waves) default_line="waves: 5" ;;
|
|
254
|
-
persistence) default_line="persistence: true" ;;
|
|
255
|
-
enforcement) default_line="enforcement: warn" ;;
|
|
256
|
-
esac
|
|
257
|
-
# Insert after `## Session Config` line if not already present.
|
|
258
|
-
grep -q "^$field:" "$CONFIG_FILE" \
|
|
259
|
-
|| awk -v insert="$default_line" '/^## Session Config/ && !done { print; print ""; print insert; done=1; next } { print }' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" \
|
|
260
|
-
&& mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
|
|
261
|
-
done
|
|
262
|
-
echo "Patched $CONFIG_FILE with defaults for: $MISSING_FIELDS"
|
|
263
|
-
fi
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
This patch is best-effort: existing fields are never overwritten. If no fields are missing, this step is a no-op.
|
|
267
|
-
|
|
268
|
-
7. **Commit.** Stage the lock file (and the patched config file, if it changed) and commit:
|
|
269
|
-
```bash
|
|
270
|
-
mkdir -p .orchestrator
|
|
271
|
-
git add .orchestrator/bootstrap.lock
|
|
272
|
-
# Also stage CLAUDE.md/AGENTS.md if step 6 patched it.
|
|
273
|
-
git diff --name-only --cached CLAUDE.md AGENTS.md 2>/dev/null | head -1 >/dev/null || {
|
|
274
|
-
[[ -f CLAUDE.md ]] && git diff --quiet CLAUDE.md || git add CLAUDE.md
|
|
275
|
-
[[ -f AGENTS.md ]] && git diff --quiet AGENTS.md || git add AGENTS.md
|
|
276
|
-
}
|
|
277
|
-
git commit -m "chore: bootstrap lock (retroactive)"
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
8. **Report.** Print: `Retroactive bootstrap complete. Lock written (tier: <INFERRED_TIER>, source: retroactive).` Include a second line `Patched Session Config: <fields>` when step 6 applied any patches, otherwise `No config changes.`.
|
|
187
|
+
**Read WHEN:** `$ARGUMENTS` contains `--retroactive`.
|
|
281
188
|
|
|
282
189
|
---
|
|
283
190
|
|
|
284
191
|
## Refresh-Lock Flow (`--refresh-lock`)
|
|
285
192
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
**Purpose (#57):** Acknowledge the current plugin version and reset the freshness clock on an existing, already-valid `bootstrap.lock` without disturbing its original bootstrap provenance. This closes the gap left by the Retroactive Flow: once a lock already has `version` + `tier`, re-running `/bootstrap --retroactive` reports "bootstrap.lock already present ... Nothing to do." and changes nothing — exactly the no-op the bootstrap-lock-freshness probe (#186/#290) was recommending as its remediation. `--refresh-lock` is the actual remediation for a present-but-stale or version-drifted lock.
|
|
193
|
+
Acknowledges the current plugin version and resets the freshness clock on an already-valid `bootstrap.lock` (`refreshed-at` + `refreshed-plugin-version`) without disturbing its original bootstrap provenance or re-running scaffolding.
|
|
289
194
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
1. **Precondition check.** Read `.orchestrator/bootstrap.lock`. If missing, or present but missing a non-empty `version` or `tier` field, abort with: `Error: No valid bootstrap.lock found. Run /bootstrap or /bootstrap --retroactive first.` Do not fabricate a lock — this flow only refreshes an existing one.
|
|
293
|
-
|
|
294
|
-
2. **Resolve the current plugin version.** Read `plugin-version` from `$PLUGIN_ROOT/package.json` (same source Phase 4 uses).
|
|
295
|
-
|
|
296
|
-
3. **Call the refresh writer.**
|
|
297
|
-
|
|
298
|
-
```js
|
|
299
|
-
import { refreshBootstrapLock } from '$PLUGIN_ROOT/scripts/lib/bootstrap-lock-refresh.mjs';
|
|
300
|
-
const result = refreshBootstrapLock({
|
|
301
|
-
repoRoot: REPO_ROOT,
|
|
302
|
-
currentPluginVersion: PLUGIN_VERSION,
|
|
303
|
-
});
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
`refreshBootstrapLock` writes (or replaces, if already present) exactly two lines — `refreshed-at: <ISO 8601 UTC>` and `refreshed-plugin-version: <current plugin version>` — via the same atomic tmp-file + rename pattern used by the Retroactive Flow's lock write: write to a sibling tmp file, then rename over the target so the lock is never observed half-written. **Every other line of the lock — `bootstrapped-at`, `timestamp`, `plugin-version`, `tier`, `archetype`, `source`, … — is left byte-identical.** This is the provenance-honesty guarantee: a refresh is an acknowledgement, not a re-bootstrap. On failure (`result.ok === false`), surface `result.message` and stop — do not retry with a fabricated lock.
|
|
195
|
+
See [references/bootstrap-refresh-lock-flow.md](references/bootstrap-refresh-lock-flow.md).
|
|
307
196
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
5. **Report.** Print: `Lock refreshed (refreshed-at: <now>, plugin-version: <current>). Original bootstrap provenance unchanged.`
|
|
311
|
-
|
|
312
|
-
**Idempotency.** Running `/bootstrap --refresh-lock` twice in a row replaces the same two lines in place — it never duplicates them.
|
|
197
|
+
**Read WHEN:** `$ARGUMENTS` contains `--refresh-lock`.
|
|
313
198
|
|
|
314
199
|
---
|
|
315
200
|
|
|
@@ -477,103 +362,21 @@ if (!isDispatcherAutonomyBlockPresent(content)) {
|
|
|
477
362
|
|
|
478
363
|
## Phase 3.6: (Optional) Rules-Fetch Bridge
|
|
479
364
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
After scaffolding, the Standard and Deep templates execute S99. On the private
|
|
483
|
-
path, it applies the selected contract's local rule union, rechecks conditional
|
|
484
|
-
dependencies and preserves existing files. It excludes every plugin-owned
|
|
485
|
-
basename and aborts on an invalid configured contract; see `private-contract.md`.
|
|
486
|
-
|
|
487
|
-
On the public path, S99 retains the optional remote rules-fetch step. It pulls
|
|
488
|
-
canonical `.claude/rules/*.md` directly from the configured baseline GitLab
|
|
489
|
-
project, excluding all plugin-owned basenames. The remote step only fires when:
|
|
490
|
-
|
|
491
|
-
- `baseline-ref` is present in Session Config
|
|
492
|
-
- `GITLAB_TOKEN` env var is set
|
|
493
|
-
- `scripts/lib/fetch-baseline.mjs` is present in the plugin
|
|
494
|
-
- A GitLab host is resolvable from the `gitlab-host` Session Config key (or the `GITLAB_HOST` env var) — never a hardcoded default
|
|
495
|
-
|
|
496
|
-
When triggered, the step:
|
|
365
|
+
Pulls canonical `.claude/rules/*.md` from the configured baseline GitLab project on the public path (or applies the private contract's local rule union), writes `.claude/.baseline-fetch.lock`, and falls back to the legacy Clank sync flow on any fetch failure.
|
|
497
366
|
|
|
498
|
-
|
|
499
|
-
2. Fetches each rule listed in the default manifest from the configured `baseline-project-id` (default `52`) at the configured `baseline-ref`
|
|
500
|
-
3. Writes `.claude/.baseline-fetch.lock` (via an inline `node --input-type=module -e`) recording what was fetched
|
|
501
|
-
4. Populates `.claude/.baseline-cache/` for offline fallback on subsequent invocations
|
|
367
|
+
See [references/bootstrap-rules-fetch-bridge.md](references/bootstrap-rules-fetch-bridge.md).
|
|
502
368
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
**Why opt-in:** Repos without `baseline-ref` continue to receive rules via the existing Clank sync flow. The fetch bridge is a faster on-demand alternative for newly-bootstrapped repos that want current rules immediately.
|
|
506
|
-
|
|
507
|
-
**Local edits:** Re-running bootstrap with `baseline-ref` set will overwrite `.claude/rules/*.md` (rules are canonical). Repo-specific extensions belong in `.claude/rules/local/*.md` (not fetched, not overwritten).
|
|
508
|
-
|
|
509
|
-
See `standard-template.md` (Step S99) and `deep-template.md` (Step D99) for the implementation, and `docs/session-config-reference.md` for the `baseline-ref` and `baseline-project-id` field definitions.
|
|
510
|
-
|
|
511
|
-
### `.claude/.baseline-fetch.lock` Schema
|
|
512
|
-
|
|
513
|
-
The lock file is committed to git and records what was fetched.
|
|
514
|
-
|
|
515
|
-
```yaml
|
|
516
|
-
# .claude/.baseline-fetch.lock
|
|
517
|
-
version: 1
|
|
518
|
-
project_id: 52
|
|
519
|
-
baseline_ref: main
|
|
520
|
-
fetched_at: 2026-04-17T13:42:00Z # ISO 8601 UTC
|
|
521
|
-
files:
|
|
522
|
-
- .claude/rules/development.md
|
|
523
|
-
- .claude/rules/security.md
|
|
524
|
-
- .claude/rules/...
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
| Field | Description |
|
|
528
|
-
|---|---|
|
|
529
|
-
| `version` | Lock file schema version. Currently `1`. |
|
|
530
|
-
| `project_id` | GitLab project ID the files were fetched from. |
|
|
531
|
-
| `baseline_ref` | The git ref (branch/tag/SHA) at fetch time. |
|
|
532
|
-
| `fetched_at` | ISO 8601 UTC timestamp. |
|
|
533
|
-
| `files` | List of fetched file paths (relative to repo root). |
|
|
369
|
+
**Read WHEN:** Phase 3 (Dispatch to Template) reaches step S99/D99, or when investigating `.claude/.baseline-fetch.lock` contents.
|
|
534
370
|
|
|
535
371
|
---
|
|
536
372
|
|
|
537
373
|
## Ecosystem-Health Flow (`--ecosystem-health`)
|
|
538
374
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
**Purpose:** Populate the `health-endpoints`, `pipelines`, and `criticalIssueLabels` configuration consumed by `skills/ecosystem-health/SKILL.md`. Runs the interactive wizard in `scripts/lib/ecosystem-wizard.mjs`, which detects CI provider + package manager automatically and prompts the user for the remaining values.
|
|
542
|
-
|
|
543
|
-
**Steps:**
|
|
544
|
-
|
|
545
|
-
1. **Run the wizard.**
|
|
546
|
-
|
|
547
|
-
```bash
|
|
548
|
-
node "$PLUGIN_ROOT/scripts/lib/ecosystem-wizard.mjs" --repo-root "$(pwd)"
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
The wizard will:
|
|
552
|
-
- Detect CI provider (`.gitlab-ci.yml` → `gitlab`; `.github/workflows/` → `github`; else `none`)
|
|
553
|
-
- Detect package manager from lockfile
|
|
554
|
-
- Prompt for health endpoints (format: `Name|URL`, comma-separated)
|
|
555
|
-
- Prompt for CI pipeline identifiers (format: `id` or `id:label`, comma-separated)
|
|
556
|
-
- Prompt for critical issue labels (comma-separated strings)
|
|
557
|
-
|
|
558
|
-
2. **Wizard writes two files** (or skips each if already present):
|
|
559
|
-
- `CLAUDE.md` (or `AGENTS.md`) — appends `ecosystem-health:` block inside `## Session Config`
|
|
560
|
-
- `.orchestrator/policy/ecosystem.json` — full policy file (schema: `.orchestrator/policy/ecosystem.schema.json`)
|
|
561
|
-
|
|
562
|
-
3. **No auto-commit.** The wizard prints what it wrote. The user reviews with `git status && git diff` and commits manually.
|
|
563
|
-
|
|
564
|
-
**Report:** The wizard prints a one-line summary per file:
|
|
565
|
-
|
|
566
|
-
```
|
|
567
|
-
Ecosystem-Health Wizard complete.
|
|
568
|
-
Written: .orchestrator/policy/ecosystem.json, CLAUDE.md
|
|
569
|
-
Skipped (already present): (none)
|
|
570
|
-
|
|
571
|
-
Review changes with: git status && git diff
|
|
572
|
-
```
|
|
375
|
+
A **standalone flow** — does not scaffold repo structure or write `bootstrap.lock`. Walks the ecosystem-health wizard and writes `.orchestrator/policy/ecosystem.json`.
|
|
573
376
|
|
|
574
|
-
|
|
377
|
+
See [references/bootstrap-ecosystem-health-flow.md](references/bootstrap-ecosystem-health-flow.md).
|
|
575
378
|
|
|
576
|
-
|
|
379
|
+
**Read WHEN:** `$ARGUMENTS` contains `--ecosystem-health`.
|
|
577
380
|
|
|
578
381
|
---
|
|
579
382
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Bootstrap — Ecosystem-Health Flow (`--ecosystem-health`)
|
|
2
|
+
|
|
3
|
+
> Reference of the `bootstrap` skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
|
|
4
|
+
> **Sibling-file paths inside this body are relative to the parent directory, not to `references/`**: `SKILL.md` → `../SKILL.md`. They were deliberately NOT rewritten, so the moved bytes stay verifiable against the pre-split file.
|
|
5
|
+
|
|
6
|
+
## Ecosystem-Health Flow (`--ecosystem-health`)
|
|
7
|
+
|
|
8
|
+
Entered when `$ARGUMENTS` contains `--ecosystem-health`. This is a **standalone flow** — it does not scaffold repo structure and does not write `bootstrap.lock`. Dispatch immediately; do not proceed to Phase 1.
|
|
9
|
+
|
|
10
|
+
**Purpose:** Populate the `health-endpoints`, `pipelines`, and `criticalIssueLabels` configuration consumed by `skills/ecosystem-health/SKILL.md`. Runs the interactive wizard in `scripts/lib/ecosystem-wizard.mjs`, which detects CI provider + package manager automatically and prompts the user for the remaining values.
|
|
11
|
+
|
|
12
|
+
**Steps:**
|
|
13
|
+
|
|
14
|
+
1. **Run the wizard.**
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node "$PLUGIN_ROOT/scripts/lib/ecosystem-wizard.mjs" --repo-root "$(pwd)"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The wizard will:
|
|
21
|
+
- Detect CI provider (`.gitlab-ci.yml` → `gitlab`; `.github/workflows/` → `github`; else `none`)
|
|
22
|
+
- Detect package manager from lockfile
|
|
23
|
+
- Prompt for health endpoints (format: `Name|URL`, comma-separated)
|
|
24
|
+
- Prompt for CI pipeline identifiers (format: `id` or `id:label`, comma-separated)
|
|
25
|
+
- Prompt for critical issue labels (comma-separated strings)
|
|
26
|
+
|
|
27
|
+
2. **Wizard writes two files** (or skips each if already present):
|
|
28
|
+
- `CLAUDE.md` (or `AGENTS.md`) — appends `ecosystem-health:` block inside `## Session Config`
|
|
29
|
+
- `.orchestrator/policy/ecosystem.json` — full policy file (schema: `.orchestrator/policy/ecosystem.schema.json`)
|
|
30
|
+
|
|
31
|
+
3. **No auto-commit.** The wizard prints what it wrote. The user reviews with `git status && git diff` and commits manually.
|
|
32
|
+
|
|
33
|
+
**Report:** The wizard prints a one-line summary per file:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Ecosystem-Health Wizard complete.
|
|
37
|
+
Written: .orchestrator/policy/ecosystem.json, CLAUDE.md
|
|
38
|
+
Skipped (already present): (none)
|
|
39
|
+
|
|
40
|
+
Review changes with: git status && git diff
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Idempotency:** Safe to re-run. If both output files are already present with matching content, the wizard exits 0 with "Nothing to do." To update, remove the existing `ecosystem-health:` key from Session Config and delete `.orchestrator/policy/ecosystem.json`, then re-run.
|
|
44
|
+
|
|
45
|
+
See `skills/ecosystem-health/wizard.md` for the full prompt spec and schema details.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Bootstrap — Refresh-Lock Flow (`--refresh-lock`)
|
|
2
|
+
|
|
3
|
+
> Reference of the `bootstrap` skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
|
|
4
|
+
> **Sibling-file paths inside this body are relative to the parent directory, not to `references/`**: `SKILL.md` → `../SKILL.md`. They were deliberately NOT rewritten, so the moved bytes stay verifiable against the pre-split file.
|
|
5
|
+
|
|
6
|
+
## Refresh-Lock Flow (`--refresh-lock`)
|
|
7
|
+
|
|
8
|
+
Entered when `$ARGUMENTS` contains `--refresh-lock`. No scaffolding questions are asked, and — unlike the Retroactive Flow above — this is NOT a no-op once the lock already has valid `version`/`tier` fields: refreshing is the load-bearing action.
|
|
9
|
+
|
|
10
|
+
**Purpose (#57):** Acknowledge the current plugin version and reset the freshness clock on an existing, already-valid `bootstrap.lock` without disturbing its original bootstrap provenance. This closes the gap left by the Retroactive Flow: once a lock already has `version` + `tier`, re-running `/bootstrap --retroactive` reports "bootstrap.lock already present ... Nothing to do." and changes nothing — exactly the no-op the bootstrap-lock-freshness probe (#186/#290) was recommending as its remediation. `--refresh-lock` is the actual remediation for a present-but-stale or version-drifted lock.
|
|
11
|
+
|
|
12
|
+
**Steps:**
|
|
13
|
+
|
|
14
|
+
1. **Precondition check.** Read `.orchestrator/bootstrap.lock`. If missing, or present but missing a non-empty `version` or `tier` field, abort with: `Error: No valid bootstrap.lock found. Run /bootstrap or /bootstrap --retroactive first.` Do not fabricate a lock — this flow only refreshes an existing one.
|
|
15
|
+
|
|
16
|
+
2. **Resolve the current plugin version.** Read `plugin-version` from `$PLUGIN_ROOT/package.json` (same source Phase 4 uses).
|
|
17
|
+
|
|
18
|
+
3. **Call the refresh writer.**
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { refreshBootstrapLock } from '$PLUGIN_ROOT/scripts/lib/bootstrap-lock-refresh.mjs';
|
|
22
|
+
const result = refreshBootstrapLock({
|
|
23
|
+
repoRoot: REPO_ROOT,
|
|
24
|
+
currentPluginVersion: PLUGIN_VERSION,
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`refreshBootstrapLock` writes (or replaces, if already present) exactly two lines — `refreshed-at: <ISO 8601 UTC>` and `refreshed-plugin-version: <current plugin version>` — via the same atomic tmp-file + rename pattern used by the Retroactive Flow's lock write: write to a sibling tmp file, then rename over the target so the lock is never observed half-written. **Every other line of the lock — `bootstrapped-at`, `timestamp`, `plugin-version`, `tier`, `archetype`, `source`, … — is left byte-identical.** This is the provenance-honesty guarantee: a refresh is an acknowledgement, not a re-bootstrap. On failure (`result.ok === false`), surface `result.message` and stop — do not retry with a fabricated lock.
|
|
29
|
+
|
|
30
|
+
4. **No auto-commit.** Unlike the Retroactive Flow, `--refresh-lock` does not stage or commit. The refreshed lock is a small, reviewable diff (two changed/added lines); the user commits it alongside their own work at their own cadence.
|
|
31
|
+
|
|
32
|
+
5. **Report.** Print: `Lock refreshed (refreshed-at: <now>, plugin-version: <current>). Original bootstrap provenance unchanged.`
|
|
33
|
+
|
|
34
|
+
**Idempotency.** Running `/bootstrap --refresh-lock` twice in a row replaces the same two lines in place — it never duplicates them.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|