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 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.5",
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.9",
102
- "peaks-loop-shared": "0.0.35",
103
- "peaks-loop-shared-channel": "0.0.14"
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
  }