peaks-loop 4.0.5 → 4.0.6
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/CHANGELOG.md +30 -0
- package/package.json +4 -4
- package/scripts/install-skills.mjs +171 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.6 — 2026-08-02 (postinstall auto-register outputStyle)
|
|
4
|
+
|
|
5
|
+
- **postinstall auto-registers `outputStyle: peaks-skill-swarm` into `~/.claude/settings.json`.**
|
|
6
|
+
|
|
7
|
+
Real user feedback 2026-08-02 (fresh 0-1 project on `Desktop\ticket-cross`): `npm i -g peaks-loop@latest` copied `peaks-skill-swarm.md` into `~/.claude/output-styles/` but never wrote `outputStyle` into `~/.claude/settings.json`, so Claude Code loaded the default style in new sessions.
|
|
8
|
+
|
|
9
|
+
The new `installBundledOutputStyleDefault()` helper in `scripts/install-skills.mjs`:
|
|
10
|
+
|
|
11
|
+
- reads `~/.claude/settings.json` (creates it if missing)
|
|
12
|
+
- merges `outputStyle: 'peaks-skill-swarm'` when not yet set
|
|
13
|
+
- preserves every other key (theme, env, plugins, hooks, permissions, ...)
|
|
14
|
+
- user-defined `outputStyle` wins — never overwritten
|
|
15
|
+
- malformed JSON / non-regular file / missing bundled style at the dispatched target → soft skip, never throw
|
|
16
|
+
|
|
17
|
+
Resolution precedence (settings file path):
|
|
18
|
+
|
|
19
|
+
1. `options.settingsFile` (test hook)
|
|
20
|
+
2. `PEAKS_CLAUDE_SETTINGS_FILE` env var (CI override)
|
|
21
|
+
3. `<homedir>/.claude/settings.json` (default)
|
|
22
|
+
|
|
23
|
+
Opt-out: `PEAKS_SKIP_OUTPUT_STYLE_DEFAULT=1`.
|
|
24
|
+
|
|
25
|
+
Tests: 6 new cases (4 integration + 2 direct unit) in `tests/integration/ide/install-skills-dispatch.test.ts`. `tests/integration/ide` 12/12 green; `pnpm build` → `build-integrity: OK`.
|
|
26
|
+
|
|
27
|
+
Compatibility:
|
|
28
|
+
|
|
29
|
+
- Existing user-defined `outputStyle` is preserved.
|
|
30
|
+
- Opt-out via `PEAKS_SKIP_OUTPUT_STYLE_DEFAULT=1` for CI / scripting.
|
|
31
|
+
- `~/.claude/settings.json` writes are atomic (`writeFileAtomically` + `O_NOFOLLOW`).
|
|
32
|
+
|
|
3
33
|
## 4.0.5 — 2026-08-02 (statusline polish + sub-agent merge bundle)
|
|
4
34
|
|
|
5
35
|
- Release 4.0.5 — 2026-08-01 bundle.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.6",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
"headroom-ai": "0.22.4",
|
|
99
99
|
"yaml": "^2.9.0",
|
|
100
100
|
"zod": "^3.25.76",
|
|
101
|
-
"peaks-loop-mut": "0.1.
|
|
102
|
-
"peaks-loop-shared": "0.0.
|
|
103
|
-
"peaks-loop-shared
|
|
101
|
+
"peaks-loop-mut": "0.1.11",
|
|
102
|
+
"peaks-loop-shared-channel": "0.0.15",
|
|
103
|
+
"peaks-loop-shared": "0.0.36"
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@alibaba-group/open-code-review": "1.3.1"
|
|
@@ -762,6 +762,143 @@ export function installBundledOutputStyles(options = {}) {
|
|
|
762
762
|
return { installed, skipped };
|
|
763
763
|
}
|
|
764
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Slice 2026-08-02 — auto-register bundled output style.
|
|
767
|
+
*
|
|
768
|
+
* Real user feedback (2026-08-02): `npm i -g peaks-loop@latest` on a
|
|
769
|
+
* fresh 0-1 project copied `peaks-skill-swarm.md` into
|
|
770
|
+
* `~/.claude/output-styles/` but never wrote
|
|
771
|
+
* `outputStyle: 'peaks-skill-swarm'` into `~/.claude/settings.json`.
|
|
772
|
+
* Result: Claude Code loaded no Peaks output style in new sessions.
|
|
773
|
+
*
|
|
774
|
+
* The fix: when `peaks-skill-swarm.md` is present in the bundled output
|
|
775
|
+
* styles directory AND `~/.claude/settings.json` does NOT yet declare
|
|
776
|
+
* `outputStyle`, merge in `outputStyle: 'peaks-skill-swarm'` while
|
|
777
|
+
* preserving every other key. If the user already set `outputStyle`,
|
|
778
|
+
* leave it alone — the user is the source of truth.
|
|
779
|
+
*
|
|
780
|
+
* Resolution precedence:
|
|
781
|
+
* 1. options.settingsFile (explicit override / test hook)
|
|
782
|
+
* 2. PEAKS_CLAUDE_SETTINGS_FILE env var (CI override)
|
|
783
|
+
* 3. homedir-relative `<homedir>/.claude/settings.json` (default)
|
|
784
|
+
*
|
|
785
|
+
* Soft-fail contract: every error path (missing file, malformed JSON,
|
|
786
|
+
* missing bundled style file, permission denied) is logged to stderr
|
|
787
|
+
* and returns `{ skipped: true, ... }` instead of throwing. The
|
|
788
|
+
* postinstall must never fail on this step because the bundled file
|
|
789
|
+
* itself was already written successfully.
|
|
790
|
+
*/
|
|
791
|
+
function resolveSettingsFilePath(options = {}) {
|
|
792
|
+
if (typeof options.settingsFile === 'string' && options.settingsFile.length > 0) {
|
|
793
|
+
return resolve(options.settingsFile);
|
|
794
|
+
}
|
|
795
|
+
const envOverride = process.env.PEAKS_CLAUDE_SETTINGS_FILE;
|
|
796
|
+
if (typeof envOverride === 'string' && envOverride.length > 0) {
|
|
797
|
+
return resolve(envOverride);
|
|
798
|
+
}
|
|
799
|
+
return resolve(homedir(), '.claude', 'settings.json');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function installBundledOutputStyleDefault(options = {}) {
|
|
803
|
+
if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || process.env.PEAKS_SKIP_OUTPUT_STYLE_DEFAULT === '1') {
|
|
804
|
+
return { skipped: true, reason: 'PEAKS_SKIP_OUTPUT_STYLE_DEFAULT=1' };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const bundledStylesDir = resolve(
|
|
808
|
+
options.targetRoot
|
|
809
|
+
?? process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR
|
|
810
|
+
?? join(homedir(), '.claude', 'output-styles')
|
|
811
|
+
);
|
|
812
|
+
const settingsPath = resolveSettingsFilePath(options);
|
|
813
|
+
const bundledStyleName = 'peaks-skill-swarm.md';
|
|
814
|
+
const bundledStylePath = join(bundledStylesDir, bundledStyleName);
|
|
815
|
+
|
|
816
|
+
// Read the existing settings.json (if any) FIRST so a malformed file
|
|
817
|
+
// surfaces as a parse error (more actionable to the user than the
|
|
818
|
+
// generic "bundled style not present" message). Malformed JSON is a
|
|
819
|
+
// user-authored file we MUST NOT clobber — log and skip.
|
|
820
|
+
let existing = null;
|
|
821
|
+
if (existsSync(settingsPath)) {
|
|
822
|
+
const stats = getPathStats(settingsPath);
|
|
823
|
+
if (!stats || !stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
824
|
+
process.stderr.write(
|
|
825
|
+
`peaks install-skills: refusing to overwrite non-regular settings.json at ${settingsPath}\n`
|
|
826
|
+
);
|
|
827
|
+
return { skipped: true, reason: 'settings.json is not a regular file' };
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const raw = readFileSafely(settingsPath, 'Claude settings.json path changed during read');
|
|
831
|
+
const parsed = JSON.parse(raw);
|
|
832
|
+
if (!isPlainObject(parsed)) {
|
|
833
|
+
process.stderr.write(
|
|
834
|
+
`peaks install-skills: refusing to overwrite settings.json that is not a JSON object at ${settingsPath}\n`
|
|
835
|
+
);
|
|
836
|
+
return { skipped: true, reason: 'settings.json is not a JSON object' };
|
|
837
|
+
}
|
|
838
|
+
existing = parsed;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
process.stderr.write(
|
|
842
|
+
`peaks install-skills: failed to parse settings.json at ${settingsPath}: ${message}; leaving untouched\n`
|
|
843
|
+
);
|
|
844
|
+
return { skipped: true, reason: 'settings.json parse error' };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Pre-condition: the bundled style file must be present in the
|
|
849
|
+
// dispatched target. If it's not, we MUST NOT inject
|
|
850
|
+
// `outputStyle: 'peaks-skill-swarm'` — Claude Code would then fail
|
|
851
|
+
// to load the style and fall back to default anyway, leaving the
|
|
852
|
+
// user with broken state. Skipping is the safe default.
|
|
853
|
+
if (!existsSync(bundledStylePath)) {
|
|
854
|
+
return { skipped: true, reason: `bundled output style not present at ${bundledStylePath}` };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// User-authored outputStyle wins — never overwrite.
|
|
858
|
+
if (existing !== null && typeof existing.outputStyle === 'string' && existing.outputStyle.length > 0) {
|
|
859
|
+
return { skipped: true, reason: `user-defined outputStyle already set: ${existing.outputStyle}` };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const next = existing === null
|
|
863
|
+
? { outputStyle: 'peaks-skill-swarm' }
|
|
864
|
+
: { ...existing, outputStyle: 'peaks-skill-swarm' };
|
|
865
|
+
|
|
866
|
+
const existingJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
|
|
867
|
+
const nextJson = `${JSON.stringify(next, null, 2)}\n`;
|
|
868
|
+
if (existingJson === nextJson) {
|
|
869
|
+
return { skipped: true, reason: 'no change' };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
writeFileAtomically(
|
|
874
|
+
settingsPath,
|
|
875
|
+
nextJson,
|
|
876
|
+
'Claude settings.json path changed during write',
|
|
877
|
+
() => {
|
|
878
|
+
if (!existsSync(settingsPath)) return;
|
|
879
|
+
const stats = getPathStats(settingsPath);
|
|
880
|
+
if (!stats || !stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
881
|
+
throw new Error('Claude settings.json path is not a regular file');
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
887
|
+
process.stderr.write(
|
|
888
|
+
`peaks install-skills: failed to write settings.json at ${settingsPath}: ${message}\n`
|
|
889
|
+
);
|
|
890
|
+
return { skipped: true, reason: 'write error', error: message };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
installed: true,
|
|
895
|
+
created: existing === null,
|
|
896
|
+
updated: existing !== null,
|
|
897
|
+
settingsPath,
|
|
898
|
+
outputStyle: 'peaks-skill-swarm',
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
765
902
|
/**
|
|
766
903
|
* Slice 7/7 — bundled agents (Claude Code sub-agent prompts).
|
|
767
904
|
*
|
|
@@ -1205,6 +1342,40 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
|
|
|
1205
1342
|
if (outputStylesResult.skipped.length > 0) {
|
|
1206
1343
|
process.stderr.write(`Peaks output styles skipped because local files already exist: ${outputStylesResult.skipped.join(', ')}\n`);
|
|
1207
1344
|
}
|
|
1345
|
+
|
|
1346
|
+
// Slice 2026-08-02 — auto-register bundled output style.
|
|
1347
|
+
//
|
|
1348
|
+
// After the bundled style file lands in `~/.claude/output-styles/`,
|
|
1349
|
+
// we also need Claude Code to actually USE it — otherwise the user
|
|
1350
|
+
// (real feedback 2026-08-02: fresh 0-1 project like
|
|
1351
|
+
// `Desktop\ticket-cross`) sees the default style in new sessions.
|
|
1352
|
+
//
|
|
1353
|
+
// This step reads `~/.claude/settings.json`, and only if the user
|
|
1354
|
+
// hasn't set `outputStyle` yet, writes
|
|
1355
|
+
// `outputStyle: 'peaks-skill-swarm'` while preserving every other
|
|
1356
|
+
// key. Malformed settings.json / IO errors / non-regular files are
|
|
1357
|
+
// soft-failed to stderr (the bundled file is already on disk, so
|
|
1358
|
+
// postinstall never aborts here).
|
|
1359
|
+
//
|
|
1360
|
+
// The `targetRoot` here mirrors the dispatch target of
|
|
1361
|
+
// `installBundledOutputStyles()` above so the env-var override
|
|
1362
|
+
// path (test fixtures + CI) stays consistent: if the bundled
|
|
1363
|
+
// style file was dispatched to an env-overridden dir, the
|
|
1364
|
+
// auto-register step checks THAT dir, not the user's homedir.
|
|
1365
|
+
try {
|
|
1366
|
+
const dispatchTargetRoot = process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR
|
|
1367
|
+
?? join(homedir(), '.claude', 'output-styles');
|
|
1368
|
+
const styleDefaultResult = installBundledOutputStyleDefault({ targetRoot: dispatchTargetRoot });
|
|
1369
|
+
if (styleDefaultResult.installed) {
|
|
1370
|
+
process.stdout.write(
|
|
1371
|
+
`Peaks output style auto-registered: ${styleDefaultResult.outputStyle}\n` +
|
|
1372
|
+
` → wrote ${styleDefaultResult.settingsPath}\n`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1377
|
+
process.stderr.write(`Peaks output style default was not installed: ${message}\n`);
|
|
1378
|
+
}
|
|
1208
1379
|
if (userConfigResult.created) {
|
|
1209
1380
|
process.stdout.write('Peaks user config created: ~/.peaks/config.json\n');
|
|
1210
1381
|
}
|