@productbrain/cli 0.1.0-beta.4804 → 0.1.0-beta.4809

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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The repo's own Product Brain config files — locating them and reading what they declare.
3
+ *
4
+ * Split out of config.ts under STD-2 / DEC-1504. The seam is deliberate and worth keeping:
5
+ * this module answers "what does THIS REPO say about itself" from the filesystem alone, while
6
+ * config.ts answers "which credential do we therefore use". Nothing here reads a credential
7
+ * store, an env var, or the global active profile — so it has no dependency on config.ts and
8
+ * cannot participate in an import cycle.
9
+ *
10
+ * Split config design:
11
+ * - config.json (committed): siteUrl only — workspace gateway, safe to share with teammates.
12
+ * - config.local.json (gitignored): profile pin — machine-local, never committed. Because it
13
+ * is gitignored, a fresh clone or worktree does NOT inherit it (INS-2462) — the caller must
14
+ * treat "no pin" as an explicit state, never as permission to guess a workspace.
15
+ */
16
+ /** Project config shape — .productbrain/config.json (committed, siteUrl only) */
17
+ export interface ProjectConfig {
18
+ siteUrl?: string;
19
+ }
20
+ /** Local project config shape — .productbrain/config.local.json (gitignored, machine-local) */
21
+ export interface LocalProjectConfig {
22
+ profile?: string;
23
+ /** Direct preview API key — set by `npm run pb:preview`, cleared by `npm run pb:real`. */
24
+ apiKey?: string;
25
+ /** Preview site URL — set alongside apiKey by `npm run pb:preview`. */
26
+ siteUrl?: string;
27
+ /** Marks this config as preview-bound; the CLI shows a loud banner on every command. */
28
+ preview?: boolean;
29
+ /** Human-readable preview workspace name for the banner (e.g. "Preview Workspace"). */
30
+ workspaceName?: string;
31
+ }
32
+ /**
33
+ * Walk up the directory tree (git-style) to find the nearest CLI project root.
34
+ *
35
+ * A `.productbrain/` directory only counts as a CLI project root when it carries
36
+ * CLI configuration — `config.json` (committed siteUrl gateway) or
37
+ * `config.local.json` (machine-local profile pin or preview binding). A
38
+ * projection-only `.productbrain/` written by `pb handshake` (briefing.md,
39
+ * context.md, rules/, etc.) does NOT count — otherwise a subrepo like
40
+ * `packages/cli/.productbrain/` would shadow the real configuring root and
41
+ * `npm run pb:preview` writes (which always land at the repo-root
42
+ * `.productbrain/`) would be invisible to commands run from the subrepo.
43
+ *
44
+ * Stops at home directory or filesystem root. Returns the directory containing
45
+ * a config-bearing `.productbrain/`, or null if not found.
46
+ */
47
+ export declare function findProjectRoot(startDir: string): string | null;
48
+ /**
49
+ * Read .productbrain/config.json from the walked-up project root.
50
+ * Only reads siteUrl — profile and apiKey fields are ignored (legacy or misplaced).
51
+ * Returns null if file missing or project root not found.
52
+ * Warns to stderr on invalid JSON but does not throw.
53
+ */
54
+ export declare function readProjectConfig(): ProjectConfig | null;
55
+ /**
56
+ * Read .productbrain/config.local.json from the walked-up project root.
57
+ * Returns the machine-local pin (`profile`) and any preview-binding fields
58
+ * (`apiKey`, `siteUrl`, `preview`, `workspaceName`) written by
59
+ * `npm run pb:preview` — all gitignored, never committed.
60
+ * Returns null if file missing or project root not found.
61
+ * Warns to stderr on invalid JSON but does not throw.
62
+ */
63
+ export declare function readLocalProjectConfig(): LocalProjectConfig | null;
64
+ //# sourceMappingURL=projectConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectConfig.d.ts","sourceRoot":"","sources":["../../src/lib/projectConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB/D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,GAAG,IAAI,CAuBxD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,kBAAkB,GAAG,IAAI,CAmClE"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The repo's own Product Brain config files — locating them and reading what they declare.
3
+ *
4
+ * Split out of config.ts under STD-2 / DEC-1504. The seam is deliberate and worth keeping:
5
+ * this module answers "what does THIS REPO say about itself" from the filesystem alone, while
6
+ * config.ts answers "which credential do we therefore use". Nothing here reads a credential
7
+ * store, an env var, or the global active profile — so it has no dependency on config.ts and
8
+ * cannot participate in an import cycle.
9
+ *
10
+ * Split config design:
11
+ * - config.json (committed): siteUrl only — workspace gateway, safe to share with teammates.
12
+ * - config.local.json (gitignored): profile pin — machine-local, never committed. Because it
13
+ * is gitignored, a fresh clone or worktree does NOT inherit it (INS-2462) — the caller must
14
+ * treat "no pin" as an explicit state, never as permission to guess a workspace.
15
+ */
16
+ import { readFileSync, existsSync } from 'fs';
17
+ import { resolve } from 'path';
18
+ import { homedir } from 'os';
19
+ import { API_KEY_PREFIX } from './tokenConstants.js';
20
+ /**
21
+ * Walk up the directory tree (git-style) to find the nearest CLI project root.
22
+ *
23
+ * A `.productbrain/` directory only counts as a CLI project root when it carries
24
+ * CLI configuration — `config.json` (committed siteUrl gateway) or
25
+ * `config.local.json` (machine-local profile pin or preview binding). A
26
+ * projection-only `.productbrain/` written by `pb handshake` (briefing.md,
27
+ * context.md, rules/, etc.) does NOT count — otherwise a subrepo like
28
+ * `packages/cli/.productbrain/` would shadow the real configuring root and
29
+ * `npm run pb:preview` writes (which always land at the repo-root
30
+ * `.productbrain/`) would be invisible to commands run from the subrepo.
31
+ *
32
+ * Stops at home directory or filesystem root. Returns the directory containing
33
+ * a config-bearing `.productbrain/`, or null if not found.
34
+ */
35
+ export function findProjectRoot(startDir) {
36
+ let dir = startDir;
37
+ const home = homedir();
38
+ while (true) {
39
+ const pbDir = resolve(dir, '.productbrain');
40
+ if (existsSync(pbDir) &&
41
+ (existsSync(resolve(pbDir, 'config.json')) ||
42
+ existsSync(resolve(pbDir, 'config.local.json')))) {
43
+ return dir;
44
+ }
45
+ const parent = resolve(dir, '..');
46
+ if (parent === dir || dir === home)
47
+ return null;
48
+ dir = parent;
49
+ }
50
+ }
51
+ /**
52
+ * Read .productbrain/config.json from the walked-up project root.
53
+ * Only reads siteUrl — profile and apiKey fields are ignored (legacy or misplaced).
54
+ * Returns null if file missing or project root not found.
55
+ * Warns to stderr on invalid JSON but does not throw.
56
+ */
57
+ export function readProjectConfig() {
58
+ const projectRoot = findProjectRoot(process.cwd());
59
+ if (!projectRoot)
60
+ return null;
61
+ const configPath = resolve(projectRoot, '.productbrain', 'config.json');
62
+ if (!existsSync(configPath))
63
+ return null;
64
+ try {
65
+ const raw = readFileSync(configPath, 'utf8');
66
+ const parsed = JSON.parse(raw);
67
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
68
+ process.stderr.write('Warning: .productbrain/config.json is not a JSON object, skipping.\n');
69
+ return null;
70
+ }
71
+ const result = {};
72
+ if (typeof parsed.siteUrl === 'string' && parsed.siteUrl) {
73
+ result.siteUrl = parsed.siteUrl;
74
+ }
75
+ // Silently ignore `profile` and `apiKey` in config.json — those must be in config.local.json
76
+ return result;
77
+ }
78
+ catch {
79
+ process.stderr.write('Warning: .productbrain/config.json contains invalid JSON, skipping.\n');
80
+ return null;
81
+ }
82
+ }
83
+ /**
84
+ * Read .productbrain/config.local.json from the walked-up project root.
85
+ * Returns the machine-local pin (`profile`) and any preview-binding fields
86
+ * (`apiKey`, `siteUrl`, `preview`, `workspaceName`) written by
87
+ * `npm run pb:preview` — all gitignored, never committed.
88
+ * Returns null if file missing or project root not found.
89
+ * Warns to stderr on invalid JSON but does not throw.
90
+ */
91
+ export function readLocalProjectConfig() {
92
+ const projectRoot = findProjectRoot(process.cwd());
93
+ if (!projectRoot)
94
+ return null;
95
+ const configPath = resolve(projectRoot, '.productbrain', 'config.local.json');
96
+ if (!existsSync(configPath))
97
+ return null;
98
+ try {
99
+ const raw = readFileSync(configPath, 'utf8');
100
+ const parsed = JSON.parse(raw);
101
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
102
+ process.stderr.write('Warning: .productbrain/config.local.json is not a JSON object, skipping.\n');
103
+ return null;
104
+ }
105
+ const result = {};
106
+ if (typeof parsed.profile === 'string' && parsed.profile) {
107
+ result.profile = parsed.profile;
108
+ }
109
+ // Preview binding fields — written by `npm run pb:preview`, cleared by `npm run pb:real`.
110
+ if (typeof parsed.apiKey === 'string' && parsed.apiKey.startsWith(API_KEY_PREFIX)) {
111
+ result.apiKey = parsed.apiKey;
112
+ }
113
+ if (typeof parsed.siteUrl === 'string' && parsed.siteUrl) {
114
+ result.siteUrl = parsed.siteUrl;
115
+ }
116
+ if (parsed.preview === true) {
117
+ result.preview = true;
118
+ }
119
+ if (typeof parsed.workspaceName === 'string' && parsed.workspaceName) {
120
+ result.workspaceName = parsed.workspaceName;
121
+ }
122
+ return result;
123
+ }
124
+ catch {
125
+ process.stderr.write('Warning: .productbrain/config.local.json contains invalid JSON, skipping.\n');
126
+ return null;
127
+ }
128
+ }
129
+ //# sourceMappingURL=projectConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectConfig.js","sourceRoot":"","sources":["../../src/lib/projectConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAoBrD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,IAAI,GAAG,GAAG,QAAQ,CAAC;IACnB,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;QAC5C,IACE,UAAU,CAAC,KAAK,CAAC;YACjB,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;gBACxC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,EAClD,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,GAAG,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAChD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACnD,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC;IACxE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sEAAsE,CAAC,CAAC;YAC7F,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,MAAM,GAAkB,EAAE,CAAC;QACjC,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACzD,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAClC,CAAC;QACD,6FAA6F;QAC7F,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC9F,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACnD,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,eAAe,EAAE,mBAAmB,CAAC,CAAC;IAC9E,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;YACnG,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,MAAM,GAAuB,EAAE,CAAC;QACtC,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACzD,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAClC,CAAC;QACD,0FAA0F;QAC1F,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;YAClF,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAChC,CAAC;QACD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACzD,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAClC,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC5B,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;YACrE,MAAM,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;QAC9C,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6EAA6E,CAAC,CAAC;QACpG,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -18,11 +18,15 @@
18
18
  * label and its qualifier can never contradict — and there is no per-write network
19
19
  * round-trip in front of the write.
20
20
  *
21
- * Output contract — identical to capture.ts's grounding banner (the JSON-safety pattern):
22
- * - The notice is HUMAN-ONLY: written to STDERR, gated by the combined `json` flag.
23
- * - In `--json` mode it is suppressed entirely, so strict-JSON stdout stays
24
- * byte-for-byte unchanged (machine consumers never see it). The gate is the `json`
25
- * flag alone not `--quiet`: a quiet human run still names its target Chain.
21
+ * Output contract — follows capture.ts's grounding banner (the JSON-safety pattern):
22
+ * - The notice is written to STDERR, never stdout, so strict-JSON stdout is byte-for-byte
23
+ * unchanged in every mode.
24
+ * - `--json` suppresses only the PLAIN line for an explicit binding (no warning to carry,
25
+ * pure noise in agent logs). The unpinned-fallback WARNING is emitted in json mode too:
26
+ * agents invoke pb with `--json` and do most of the writing, so suppressing it there
27
+ * disabled this guard for its primary audience (TEN-3238).
28
+ * - The gate is the `json` flag alone — not `--quiet`: a quiet human run still names its
29
+ * target Chain.
26
30
  *
27
31
  * SSOT: one pure formatter + one thin emitter, called from all four write commands —
28
32
  * no per-command copy-paste.
@@ -47,9 +51,17 @@ export interface WriteTargetNoticeInput {
47
51
  */
48
52
  export declare function formatWriteTargetNotice({ profile, source }: WriteTargetNoticeInput): string | null;
49
53
  /**
50
- * Resolve identity and write the target notice to STDERR gated by the combined `json`
51
- * flag. Pass the SAME value the command computed (e.g. `isJsonMode() || options.json ===
52
- * true`) so `--json` stdout stays byte-for-byte unchanged.
54
+ * Resolve identity and write the target notice to STDERR. Pass the SAME `json` value the
55
+ * command computed (e.g. `isJsonMode() || options.json === true`).
56
+ *
57
+ * `json` suppresses only the PLAIN line for an explicit binding. The unpinned-fallback
58
+ * WARNING is never suppressed (TEN-3238): `--json` is how agents invoke pb, and agents are
59
+ * the bulk of Chain writers, so gating the warning on `json` switched this guard off in
60
+ * exactly the mode that needed it — a wrong-workspace write from an unpinned directory was
61
+ * silent for every agent. Emitting it in json mode is safe because it goes to STDERR, so
62
+ * strict JSON on stdout stays byte-for-byte unchanged either way (and the CLI already writes
63
+ * unconditional stderr warnings elsewhere, e.g. the preview-scrub misroute in config.ts).
64
+ * The plain line stays human-only: it carries no warning, so it is pure noise in agent logs.
53
65
  *
54
66
  * Never throws and does no network IO: a wrong/unreadable config degrades to silence, and
55
67
  * the label comes from `resolveEffectiveProfile()` alone — only fast local config reads
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceNotice.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceNotice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAA2B,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAEnF,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC,yFAAyF;IACzF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oEAAoE;IACpE,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAQlG;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAcxE"}
1
+ {"version":3,"file":"workspaceNotice.d.ts","sourceRoot":"","sources":["../../src/lib/workspaceNotice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAA2B,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAEnF,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC,yFAAyF;IACzF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oEAAoE;IACpE,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAQlG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAexE"}
@@ -18,11 +18,15 @@
18
18
  * label and its qualifier can never contradict — and there is no per-write network
19
19
  * round-trip in front of the write.
20
20
  *
21
- * Output contract — identical to capture.ts's grounding banner (the JSON-safety pattern):
22
- * - The notice is HUMAN-ONLY: written to STDERR, gated by the combined `json` flag.
23
- * - In `--json` mode it is suppressed entirely, so strict-JSON stdout stays
24
- * byte-for-byte unchanged (machine consumers never see it). The gate is the `json`
25
- * flag alone not `--quiet`: a quiet human run still names its target Chain.
21
+ * Output contract — follows capture.ts's grounding banner (the JSON-safety pattern):
22
+ * - The notice is written to STDERR, never stdout, so strict-JSON stdout is byte-for-byte
23
+ * unchanged in every mode.
24
+ * - `--json` suppresses only the PLAIN line for an explicit binding (no warning to carry,
25
+ * pure noise in agent logs). The unpinned-fallback WARNING is emitted in json mode too:
26
+ * agents invoke pb with `--json` and do most of the writing, so suppressing it there
27
+ * disabled this guard for its primary audience (TEN-3238).
28
+ * - The gate is the `json` flag alone — not `--quiet`: a quiet human run still names its
29
+ * target Chain.
26
30
  *
27
31
  * SSOT: one pure formatter + one thin emitter, called from all four write commands —
28
32
  * no per-command copy-paste.
@@ -49,17 +53,23 @@ export function formatWriteTargetNotice({ profile, source }) {
49
53
  return `→ ${name}`;
50
54
  }
51
55
  /**
52
- * Resolve identity and write the target notice to STDERR gated by the combined `json`
53
- * flag. Pass the SAME value the command computed (e.g. `isJsonMode() || options.json ===
54
- * true`) so `--json` stdout stays byte-for-byte unchanged.
56
+ * Resolve identity and write the target notice to STDERR. Pass the SAME `json` value the
57
+ * command computed (e.g. `isJsonMode() || options.json === true`).
58
+ *
59
+ * `json` suppresses only the PLAIN line for an explicit binding. The unpinned-fallback
60
+ * WARNING is never suppressed (TEN-3238): `--json` is how agents invoke pb, and agents are
61
+ * the bulk of Chain writers, so gating the warning on `json` switched this guard off in
62
+ * exactly the mode that needed it — a wrong-workspace write from an unpinned directory was
63
+ * silent for every agent. Emitting it in json mode is safe because it goes to STDERR, so
64
+ * strict JSON on stdout stays byte-for-byte unchanged either way (and the CLI already writes
65
+ * unconditional stderr warnings elsewhere, e.g. the preview-scrub misroute in config.ts).
66
+ * The plain line stays human-only: it carries no warning, so it is pure noise in agent logs.
55
67
  *
56
68
  * Never throws and does no network IO: a wrong/unreadable config degrades to silence, and
57
69
  * the label comes from `resolveEffectiveProfile()` alone — only fast local config reads
58
70
  * (no probe) in front of the write. It is safe to call immediately before any Chain write.
59
71
  */
60
72
  export async function emitWriteTargetNotice(json) {
61
- if (json)
62
- return; // strict-JSON stdout stays byte-for-byte unchanged.
63
73
  let profile = null;
64
74
  let source = 'none';
65
75
  try {
@@ -70,6 +80,9 @@ export async function emitWriteTargetNotice(json) {
70
80
  }
71
81
  if (source === 'none')
72
82
  return; // not configured — nothing to name.
83
+ const isUnpinnedFallback = source === 'global' || source === 'default';
84
+ if (json && !isUnpinnedFallback)
85
+ return;
73
86
  const notice = formatWriteTargetNotice({ profile, source });
74
87
  if (notice)
75
88
  process.stderr.write(notice + '\n');
@@ -1 +1 @@
1
- {"version":3,"file":"workspaceNotice.js","sourceRoot":"","sources":["../../src/lib/workspaceNotice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,uBAAuB,EAA+B,MAAM,aAAa,CAAC;AAUnF;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAA0B;IACjF,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IAC1C,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAChD,OAAO,KAAK,IAAI,oEAAoE,CAAC;IACvF,CAAC;IACD,sDAAsD;IACtD,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAa;IACvD,IAAI,IAAI;QAAE,OAAO,CAAC,oDAAoD;IAEtE,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,IAAI,MAAM,GAA2B,MAAM,CAAC;IAC5C,IAAI,CAAC;QACH,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,uBAAuB,EAAE,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,oEAAoE;IAC9E,CAAC;IACD,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,CAAC,oCAAoC;IAEnE,MAAM,MAAM,GAAG,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5D,IAAI,MAAM;QAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAClD,CAAC"}
1
+ {"version":3,"file":"workspaceNotice.js","sourceRoot":"","sources":["../../src/lib/workspaceNotice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,uBAAuB,EAA+B,MAAM,aAAa,CAAC;AAUnF;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAA0B;IACjF,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IAC1C,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAChD,OAAO,KAAK,IAAI,oEAAoE,CAAC;IACvF,CAAC;IACD,sDAAsD;IACtD,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAa;IACvD,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,IAAI,MAAM,GAA2B,MAAM,CAAC;IAC5C,IAAI,CAAC;QACH,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,uBAAuB,EAAE,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,oEAAoE;IAC9E,CAAC;IACD,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,CAAC,oCAAoC;IAEnE,MAAM,kBAAkB,GAAG,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,SAAS,CAAC;IACvE,IAAI,IAAI,IAAI,CAAC,kBAAkB;QAAE,OAAO;IAExC,MAAM,MAAM,GAAG,uBAAuB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5D,IAAI,MAAM;QAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAClD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productbrain/cli",
3
- "version": "0.1.0-beta.4804",
3
+ "version": "0.1.0-beta.4809",
4
4
  "description": "Product Brain — Chain knowledge and write-back CLI",
5
5
  "type": "module",
6
6
  "bin": {