peaks-loop 4.0.0-beta.20 → 4.0.0-beta.21

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
@@ -5,22 +5,96 @@
5
5
  ### Patch Changes
6
6
 
7
7
  - Updated dependencies
8
- - peaks-loop-shared@0.0.9
9
- - peaks-loop-audit-independent@0.0.5
10
- - peaks-loop-crystallization@0.0.6
11
- - peaks-loop-doctor@0.0.5
12
- - peaks-loop-final-review@0.0.5
8
+ - peaks-loop-shared@0.0.15
9
+ - peaks-loop-audit-independent@0.0.11
10
+ - peaks-loop-crystallization@0.0.12
11
+ - peaks-loop-doctor@0.0.11
12
+ - peaks-loop-final-review@0.0.11
13
13
 
14
14
  ## 4.0.0
15
15
 
16
16
  ### Patch Changes
17
17
 
18
18
  - Updated dependencies
19
- - peaks-loop-shared@0.0.5
20
- - peaks-loop-audit-independent@0.0.5
21
- - peaks-loop-crystallization@0.0.5
22
- - peaks-loop-doctor@0.0.5
23
- - peaks-loop-final-review@0.0.5
19
+ - peaks-loop-shared@0.0.15
20
+ - peaks-loop-audit-independent@0.0.11
21
+ - peaks-loop-crystallization@0.0.12
22
+ - peaks-loop-doctor@0.0.11
23
+ - peaks-loop-final-review@0.0.11
24
+
25
+ ## 4.0.0-beta.21 — 2026-07-22 (ice-cola surface-check fixes, republish)
26
+
27
+ This release is the **republish** after the accidental 4.0.2
28
+ publish earlier on 2026-07-22. The 4.0.2 manifest is being
29
+ unpublished (`npm unpublish --force`, within 72h window). The
30
+ intended release is `4.0.0-beta.21`, the version that was
31
+ manually pinned but the publish workflow's `pnpm exec changeset
32
+ version` step overrode it to `4.0.2` due to a pending
33
+ `.changeset/*.md` in the tagged commit.
34
+
35
+ ### Bug fixes
36
+
37
+ - **standardsMissing UX (`fix(standards)`)** — `peaks workspace init`
38
+ previously reported `standardsMissing.missing: true` with the
39
+ remediation text "no project-local standards found" whenever the
40
+ language overlay was empty, even when common was clearly populated
41
+ (ice-cola shape: common/\*.md + typescript overlay populated, but
42
+ no javascript/ dir). The detector now distinguishes three states
43
+ (`complete` / `common-missing` / `language-missing`) and the
44
+ remediation text calls out the precise failing component.
45
+
46
+ - **`peaks statusline` default action (`fix(statusline)`)** — `peaks
47
+ statusline` (no subcommand) was documented as "Run with no
48
+ subcommand to render" but commander was printing usage instead of
49
+ dispatching to the hidden `render` subcommand. The fix attaches
50
+ a default `.action(...)` on the parent `statusline` command that
51
+ delegates to a public `runDefaultStatuslineRender` body, and adds
52
+ `--project <path>` to the parent so the default path inherits the
53
+ project label.
54
+
55
+ - **release-pack test version pinning (`fix(release-tests)`)** —
56
+ five `tests/unit/release/*.test.ts` files hard-coded `'4.0.0-beta.17'`
57
+ and `'0.0.4'` literals. Root bumping from 4.0.0-beta.17 to
58
+ 4.0.0-beta.20 made three tests fail immediately, demonstrating the
59
+ tests were carrying version knowledge the production code does not.
60
+ Expectations now derive from the on-disk `package.json` instead,
61
+ so future version bumps are a no-op for the suite.
62
+
63
+ - **`peaks preferences` unknown-key error (`fix(preferences)`)** —
64
+ the prior error was `PREFERENCES_KEY_UNKNOWN: <key>` which forced
65
+ the operator to look up valid keys elsewhere. All three sites
66
+ (`get` / `set` / `reset`) now throw via a shared `unknownKeyError`
67
+ helper that includes the sorted `ALLOWED_KEYS_LIST` inline.
68
+
69
+ ### Chore
70
+
71
+ - **`chore(deps)`** — Migrate `onlyBuiltDependencies: [better-sqlite3]`
72
+ out of `package.json#pnpm` (no longer read by pnpm 10.11+) into
73
+ `.npmrc` as the canonical pnpm config key. Silences the per-package
74
+ `WARN The "pnpm" field in package.json is no longer read by pnpm`
75
+ log line without changing install behavior.
76
+
77
+ ### CI
78
+
79
+ - **`ci(publish)`** — Add `workflow_dispatch` input
80
+ `skip_changeset_version` to `.github/workflows/publish.yml`. When
81
+ set, the conditional `pnpm exec changeset version` step is
82
+ skipped, so manual version pins in `package.json` survive the
83
+ registry-repair release path.
84
+
85
+ - **`ci(unpublish)`** — One-shot `unpublish-4-0-2` workflow that
86
+ uses Trusted Publishing + OIDC to remove peaks-loop@4.0.2 + 8
87
+ subpackages. Triggered by `gh workflow run` with
88
+ `confirm_destructive=yes-i-am-sure`. Retired (kept for audit)
89
+ once the unpublish completes (npm 72h propagation).
90
+
91
+ ### Validation
92
+
93
+ - `vitest tests/unit/standards/missing-standards-detector.test.ts` — 18/18
94
+ - `vitest tests/unit/cli/commands/statusline-default-render.test.ts` — 3/3
95
+ - `vitest tests/unit/release` — 20/20
96
+ - `vitest tests/integration/preferences-cli.test.ts` — 5/5
97
+ - `PEAKS_DRY_RUN=1 node scripts/release-pack.mjs` — 9/9 packages, zero WARN
24
98
 
25
99
  ## 4.0.0-beta.17
26
100
 
@@ -12,6 +12,26 @@ const ALLOWED_KEYS = new Set([
12
12
  'swarmSpeculative',
13
13
  'loopAutonomousEnabled',
14
14
  ]);
15
+ /**
16
+ * Stable, sorted list of allowed preference keys. Used in error messages
17
+ * so a PREFERENCES_KEY_UNKNOWN throw is self-documenting (the operator
18
+ * sees exactly which keys are valid without reading the source).
19
+ */
20
+ const ALLOWED_KEYS_LIST = Array.from(ALLOWED_KEYS).sort();
21
+ /**
22
+ * Build a PREFERENCES_KEY_UNKNOWN error with the allowed-keys list baked
23
+ * in. Throwing a plain Error here is the legacy path that the CLI
24
+ * surfaces verbatim on stderr; the helper exists so all three sites
25
+ * (`get` / `set` / `reset`) emit the same operator-facing message.
26
+ *
27
+ * Stage-3 follow-up (ice-cola surface check 2026-07-22, follow-on to
28
+ * report item 5.3): previously the message was just `PREFERENCES_KEY_UNKNOWN:
29
+ * <key>` which forced the operator to look up valid keys elsewhere. The
30
+ * new message lists the valid keys inline so the response is actionable.
31
+ */
32
+ function unknownKeyError(key) {
33
+ return new Error(`PREFERENCES_KEY_UNKNOWN: ${key}. Allowed keys: ${ALLOWED_KEYS_LIST.join(', ')}.`);
34
+ }
15
35
  export function registerPreferencesCommands(program) {
16
36
  const prefs = program
17
37
  .command('preferences')
@@ -25,7 +45,7 @@ export function registerPreferencesCommands(program) {
25
45
  .action((opts) => {
26
46
  try {
27
47
  if (!ALLOWED_KEYS.has(opts.key)) {
28
- throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
48
+ throw unknownKeyError(opts.key);
29
49
  }
30
50
  const all = loadPreferences(opts.project);
31
51
  const value = all[opts.key];
@@ -71,7 +91,7 @@ export function registerPreferencesCommands(program) {
71
91
  .action((opts) => {
72
92
  try {
73
93
  if (!ALLOWED_KEYS.has(opts.key)) {
74
- throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
94
+ throw unknownKeyError(opts.key);
75
95
  }
76
96
  let parsed = opts.value;
77
97
  try {
@@ -106,7 +126,7 @@ export function registerPreferencesCommands(program) {
106
126
  .action((opts) => {
107
127
  try {
108
128
  if (!ALLOWED_KEYS.has(opts.key)) {
109
- throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
129
+ throw unknownKeyError(opts.key);
110
130
  }
111
131
  const filePath = preferencesPath(opts.project);
112
132
  // Read raw on-disk state (not the merged view) so removing a key
@@ -1,3 +1,15 @@
1
1
  import { Command } from 'commander';
2
2
  import { type ProgramIO } from '../cli-helpers.js';
3
+ type RenderOptions = {
4
+ project?: string;
5
+ json?: boolean;
6
+ };
7
+ /**
8
+ * Default-statusline render body. Reused by both the top-level default
9
+ * action (Bug-02 dispatch) and the explicit `render` subcommand. Exported
10
+ * so a unit test can exercise the JSON / text output paths without going
11
+ * through commander.
12
+ */
13
+ export declare function runDefaultStatuslineRender(options: RenderOptions, io: ProgramIO): Promise<void>;
3
14
  export declare function registerStatusLineCommands(program: Command, io: ProgramIO): void;
15
+ export {};
@@ -54,6 +54,26 @@ function resolveIdeForCommand(options, projectRoot) {
54
54
  }
55
55
  return detectIdeFromContext({ env: process.env, cwd: projectRoot ?? process.cwd(), parsedStdin: null });
56
56
  }
57
+ /**
58
+ * Default-statusline render body. Reused by both the top-level default
59
+ * action (Bug-02 dispatch) and the explicit `render` subcommand. Exported
60
+ * so a unit test can exercise the JSON / text output paths without going
61
+ * through commander.
62
+ */
63
+ export async function runDefaultStatuslineRender(options, io) {
64
+ const raw = await readStdin();
65
+ const stdin = parseStatusLineStdin(raw);
66
+ const seeded = options.project
67
+ ? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
68
+ : stdin;
69
+ const model = buildStatusLineModel(seeded, Date.now());
70
+ const text = renderStatusLine(model);
71
+ if (options.json === true) {
72
+ io.stdout(JSON.stringify({ ok: true, command: 'statusline.render', data: { text } }, null, 2));
73
+ return;
74
+ }
75
+ io.stdout(text);
76
+ }
57
77
  export function registerStatusLineCommands(program, io) {
58
78
  // Top-level `peaks statusline` — register as a group with subcommands
59
79
  // (install | uninstall | status). When the user runs `peaks statusline` with
@@ -65,27 +85,31 @@ export function registerStatusLineCommands(program, io) {
65
85
  // option-shadowing bug.
66
86
  const statusline = addJsonOption(program
67
87
  .command('statusline')
68
- .description('Render the Peaks skill status line for the current session, or manage the adapter-driven statusLine entry. Run with no subcommand to render; with a subcommand (install | uninstall | status) to manage.'));
69
- // Hidden render subcommand. This is the default behavior when the user types
70
- // `peaks statusline` with no subcommand. We mark it hidden so `peaks
71
- // statusline --help` does not show "render" as a subcommand.
88
+ .description('Render the Peaks skill status line for the current session, or manage the adapter-driven statusLine entry. Run with no subcommand to render; with a subcommand (install | uninstall | status) to manage.')
89
+ .option('--project <path>', 'project root path (used to label the status line when stdin is absent; applies to the default render path)'));
90
+ // Default behavior: when the user types `peaks statusline` with no
91
+ // subcommand, render the status line. Without this default action,
92
+ // commander falls back to printing usage (Bug-02, ice-cola surface check
93
+ // 2026-07-22). The hidden `render` subcommand is preserved so callers
94
+ // can still invoke it explicitly.
95
+ //
96
+ // Note (commander 12.x): when both a top-level `.action(...)` and
97
+ // subcommands exist, commander's option parser can conflate parent and
98
+ // child flags. We therefore parse `process.argv` for the bare `statusline`
99
+ // invocation here, and otherwise let the subcommand machinery handle the
100
+ // rest. The implementation is wrapped in `peekDefaultStatuslineRender` so
101
+ // it can be exercised in isolation by unit tests.
102
+ statusline.action(async (_parentOptions, command) => {
103
+ await runDefaultStatuslineRender(command.opts(), io);
104
+ });
105
+ // Hidden render subcommand. Preserved for callers that want to invoke
106
+ // render explicitly without going through the default-action dispatch.
72
107
  statusline
73
108
  .command('render', { hidden: true })
74
109
  .description('Render the Peaks skill status line for the current session (reads session JSON on stdin; honors --project for the project label).')
75
110
  .option('--project <path>', 'project root path (used to label the status line when stdin is absent)')
76
111
  .action(async (options) => {
77
- const raw = await readStdin();
78
- const stdin = parseStatusLineStdin(raw);
79
- const seeded = options.project
80
- ? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
81
- : stdin;
82
- const model = buildStatusLineModel(seeded, Date.now());
83
- const text = renderStatusLine(model);
84
- if (options.json === true) {
85
- io.stdout(JSON.stringify({ ok: true, command: 'statusline.render', data: { text } }, null, 2));
86
- return;
87
- }
88
- io.stdout(text);
112
+ await runDefaultStatuslineRender(options, io);
89
113
  });
90
114
  addJsonOption(statusline
91
115
  .command('install')
@@ -42,6 +42,31 @@ export type MissingProjectStandardsDiagnostic = {
42
42
  readonly remediation: string;
43
43
  readonly language: StandardsLanguage;
44
44
  };
45
+ /**
46
+ * List which standards tree components are missing vs populated.
47
+ *
48
+ * Returns one of three stable shapes:
49
+ * - { kind: 'complete' } — both common + language overlay populated
50
+ * - { kind: 'common-missing' } — common dir absent/empty (universal)
51
+ * - { kind: 'language-missing', language } — common ok, language overlay absent/empty
52
+ *
53
+ * The detector previously returned a single boolean that flipped to
54
+ * "missing: true" the moment the language overlay was empty, but printed
55
+ * the remediation as if BOTH common AND language were missing — a UX bug
56
+ * that surfaced in ice-cola (common=4 .md files; typescript=1 .md file;
57
+ * javascript overlay absent → label said "no project-local standards found"
58
+ * even though common was clearly populated). This split lets the CLI emit
59
+ * a precise remediation string per kind.
60
+ */
61
+ export type StandardsTreeDiagnostic = {
62
+ readonly kind: 'complete';
63
+ } | {
64
+ readonly kind: 'common-missing';
65
+ } | {
66
+ readonly kind: 'language-missing';
67
+ readonly language: StandardsLanguage;
68
+ };
69
+ export declare function diagnoseStandardsTree(projectRoot: string, language: StandardsLanguage): StandardsTreeDiagnostic;
45
70
  /**
46
71
  * Detect whether a consumer project's `.peaks/standards/` tree (2.0
47
72
  * canonical) is missing or empty.
@@ -62,6 +62,26 @@ function hasPopulatedMarkdown(dir) {
62
62
  const entries = readdirSync(dir);
63
63
  return entries.some((entry) => entry.toLowerCase().endsWith('.md'));
64
64
  }
65
+ export function diagnoseStandardsTree(projectRoot, language) {
66
+ if (!SUPPORTED_LANGUAGES.has(language)) {
67
+ throw new Error(`Unsupported standards language: ${String(language)}`);
68
+ }
69
+ const normalizedRoot = normalizeProjectRoot(projectRoot);
70
+ const commonDir = join(normalizedRoot, '.peaks', 'standards', 'common');
71
+ const languageDir = language === 'generic'
72
+ ? null
73
+ : join(normalizedRoot, '.peaks', 'standards', language);
74
+ const commonOk = hasPopulatedMarkdown(commonDir);
75
+ const languageOk = languageDir === null ? true : hasPopulatedMarkdown(languageDir);
76
+ if (commonOk && languageOk) {
77
+ return { kind: 'complete' };
78
+ }
79
+ if (!commonOk) {
80
+ return { kind: 'common-missing' };
81
+ }
82
+ // common ok, language overlay empty/missing
83
+ return { kind: 'language-missing', language };
84
+ }
65
85
  /**
66
86
  * Detect whether a consumer project's `.peaks/standards/` tree (2.0
67
87
  * canonical) is missing or empty.
@@ -84,18 +104,29 @@ export function detectMissingProjectStandards(projectRoot, language) {
84
104
  }
85
105
  const normalizedRoot = normalizeProjectRoot(projectRoot);
86
106
  const renderedPath = renderForPlatform(normalizedRoot);
87
- const commonDir = join(normalizedRoot, '.peaks', 'standards', 'common');
88
- const languageDir = language === 'generic'
89
- ? null
90
- : join(normalizedRoot, '.peaks', 'standards', language);
91
- const commonOk = hasPopulatedMarkdown(commonDir);
92
- const languageOk = languageDir === null ? true : hasPopulatedMarkdown(languageDir);
93
- const missing = !(commonOk && languageOk);
94
- // Remediation message: copy-pasteable, mentions language, references the
95
- // resolved project path, and hints at the `--init-standards` opt-in flag.
96
- const remediation = missing
97
- ? `⚠ no project-local standards found at ${renderedPath} — run \`peaks standards init --project ${normalizedRoot} --apply\` to scaffold, or pass --init-standards to peaks workspace init for one-shot auto-apply (language: ${language}).`
98
- : `Project-local standards already present at ${renderedPath}. Re-run \`peaks standards init --project ${normalizedRoot} --apply\` to refresh from the curated baseline.`;
107
+ const diagnostic = diagnoseStandardsTree(projectRoot, language);
108
+ // Bug-01 fix (ice-cola surface check 2026-07-22): the previous
109
+ // remediation said "no project-local standards found" whenever EITHER
110
+ // common or the language overlay was empty, which misrepresented
111
+ // projects that have common populated but a missing language overlay
112
+ // (e.g. ice-cola has common/*.md + typescript/coding-style.md but no
113
+ // javascript/coding-style.md). The remediation now distinguishes the
114
+ // three kinds so the operator gets a precise hint.
115
+ let missing;
116
+ let remediation;
117
+ if (diagnostic.kind === 'complete') {
118
+ missing = false;
119
+ remediation = `Project-local standards already present at ${renderedPath}. Re-run \`peaks standards init --project ${normalizedRoot} --apply\` to refresh from the curated baseline.`;
120
+ }
121
+ else if (diagnostic.kind === 'common-missing') {
122
+ missing = true;
123
+ remediation = `⚠ no project-local common standards at ${renderedPath} — run \`peaks standards init --project ${normalizedRoot} --apply\` to scaffold (language: ${language}), or pass --init-standards to peaks workspace init for one-shot auto-apply.`;
124
+ }
125
+ else {
126
+ missing = true;
127
+ const overlayName = diagnostic.language;
128
+ remediation = `⚠ common standards present at ${renderedPath}\\common but ${overlayName} language overlay is missing — run \`peaks standards init --project ${normalizedRoot} --apply\` to scaffold the ${overlayName} overlay (or pass --init-standards to peaks workspace init for one-shot auto-apply).`;
129
+ }
99
130
  return {
100
131
  missing,
101
132
  path: renderedPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.0-beta.20",
3
+ "version": "4.0.0-beta.21",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -54,14 +54,14 @@
54
54
  "headroom-ai": "0.22.4",
55
55
  "yaml": "^2.9.0",
56
56
  "zod": "^3.25.76",
57
- "peaks-loop-audit-independent": "0.0.4",
58
- "peaks-loop-doctor": "0.0.4",
59
- "peaks-loop-crystallization": "0.0.5",
60
- "peaks-loop-mut": "0.0.4",
61
- "peaks-loop-job-snapshot": "0.0.4",
62
- "peaks-loop-final-review": "0.0.4",
63
- "peaks-loop-shared-channel": "0.0.4",
64
- "peaks-loop-shared": "0.0.8"
57
+ "peaks-loop-crystallization": "0.0.11",
58
+ "peaks-loop-audit-independent": "0.0.10",
59
+ "peaks-loop-doctor": "0.0.10",
60
+ "peaks-loop-final-review": "0.0.10",
61
+ "peaks-loop-mut": "0.1.0",
62
+ "peaks-loop-job-snapshot": "0.0.5",
63
+ "peaks-loop-shared": "0.0.14",
64
+ "peaks-loop-shared-channel": "0.0.5"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@alibaba-group/open-code-review": "1.3.1"