@pareto4/pp-core 1.5.4 → 1.6.1
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/.claude-plugin/plugin.json +1 -1
- package/agents/pp-code-fixer.md +3 -3
- package/bin/install.js +0 -6
- package/commands/pp-init.md +36 -0
- package/commands/pp-ns-project.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/agent-install-check.cjs +1 -1
- package/pp-core/bin/lib/audit-command-router.cjs +3 -3
- package/pp-core/bin/lib/audit.cjs +1 -1
- package/pp-core/bin/lib/capability-activation.cjs +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +138 -5
- package/pp-core/bin/lib/capability-state.cjs +1 -1
- package/pp-core/bin/lib/capability-writer.cjs +1 -1
- package/pp-core/bin/lib/check-command-router.cjs +190 -8
- package/pp-core/bin/lib/cjs-command-router-adapter.cjs +1 -1
- package/pp-core/bin/lib/clusters.cjs +1 -0
- package/pp-core/bin/lib/code-review-flags.cjs +1 -1
- package/pp-core/bin/lib/command-aliases.cjs +7 -0
- package/pp-core/bin/lib/config-loader.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +1 -1
- package/pp-core/bin/lib/context-utilization.cjs +1 -1
- package/pp-core/bin/lib/docs.cjs +1 -1
- package/pp-core/bin/lib/frontmatter.cjs +1 -1
- package/pp-core/bin/lib/git-base-branch.cjs +2 -2
- package/pp-core/bin/lib/graphify-command-router.cjs +4 -4
- package/pp-core/bin/lib/graphify.cjs +1 -1
- package/pp-core/bin/lib/gsd2-import.cjs +1 -1
- package/pp-core/bin/lib/init-command-router.cjs +1 -1
- package/pp-core/bin/lib/init.cjs +1 -1
- package/pp-core/bin/lib/intel-command-router.cjs +5 -5
- package/pp-core/bin/lib/intel.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +4 -2
- package/pp-core/bin/lib/loop-resolver.cjs +1 -1
- package/pp-core/bin/lib/phase-command-router.cjs +1 -1
- package/pp-core/bin/lib/phases-command-router.cjs +1 -1
- package/pp-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/pp-core/bin/lib/platform-bindings.cjs +336 -0
- package/pp-core/bin/lib/profile-output.cjs +216 -1
- package/pp-core/bin/lib/prohibition-enforcement.cjs +2 -2
- package/pp-core/bin/lib/roadmap-command-router.cjs +2 -2
- package/pp-core/bin/lib/roadmap-parser.cjs +1 -1
- package/pp-core/bin/lib/state-command-router.cjs +1 -1
- package/pp-core/bin/lib/state.cjs +2 -2
- package/pp-core/bin/lib/validate-command-router.cjs +1 -1
- package/pp-core/bin/lib/verify-command-router.cjs +1 -1
- package/pp-core/bin/lib/verify.cjs +1 -1
- package/pp-core/bin/lib/worktree-base-ref.cjs +2 -2
- package/pp-core/bin/lib/worktree-safety.cjs +1 -1
- package/pp-core/bin/pp-tools.cjs +61 -24
- package/pp-core/workflows/discovery-sprint.md +27 -10
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +37 -0
- package/pp-core/workflows/execute-phase.md +4 -0
- package/pp-core/workflows/init.md +530 -0
- package/pp-core/workflows/new-project.md +54 -1
- package/pp-core/workflows/ship.md +24 -1
- package/pp-core/workflows/verify-work.md +1 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -0
- package/assets/gsd-logo-2000-transparent.png +0 -0
- package/assets/gsd-logo-2000-transparent.svg +0 -17
- package/assets/gsd-logo-2000.png +0 -0
- package/assets/gsd-logo-2000.svg +0 -21
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform Bindings — reader + hand-rolled validator for .platform-bindings.json
|
|
4
|
+
*
|
|
5
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
6
|
+
* compiles to the gitignored `pp-core/bin/lib/platform-bindings.cjs`; do NOT
|
|
7
|
+
* hand-edit the compiled output. Behaviour is LOCKED by
|
|
8
|
+
* `tests/platform-bindings.test.cjs` (the reader behaviour matrix + the
|
|
9
|
+
* schema↔validator coupling assertion).
|
|
10
|
+
*
|
|
11
|
+
* BIND-02 (D-08 single-file schema check, D-03 lenient validation):
|
|
12
|
+
* - `cmdPlatformBindings` reads `.platform-bindings.json` at the repo root
|
|
13
|
+
* (NOT under .planning/). An absent file is INERT — it emits
|
|
14
|
+
* {present:false} on exit 0 and NEVER calls error(). This makes the
|
|
15
|
+
* binding contract opt-in: every downstream Pareto behaviour can read it
|
|
16
|
+
* unconditionally and branch on `present`.
|
|
17
|
+
* - Malformed JSON is the ONLY hard-error path: it calls error() with
|
|
18
|
+
* ERROR_REASON.PLATFORM_BINDINGS_PARSE_FAILED (exit 1).
|
|
19
|
+
* - `--validate` returns the validation RESULT as data ({valid, errors}) on
|
|
20
|
+
* exit 0 — a validation verdict is never a process error.
|
|
21
|
+
* - `validateBindings` is hand-rolled (no ajv, zero new deps). It hard-checks
|
|
22
|
+
* ONLY the load-bearing core (schemaVersion, platform.repo, platform.tag)
|
|
23
|
+
* per D-03; provisional namespaces are validated leniently — an unknown or
|
|
24
|
+
* missing namespace never makes the object invalid.
|
|
25
|
+
*
|
|
26
|
+
* The core regexes below MUST mirror the `pattern` strings in
|
|
27
|
+
* `schemas/platform-bindings.schema.json`; the schema↔validator coupling test
|
|
28
|
+
* machine-enforces this so neither side can drift silently.
|
|
29
|
+
*/
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.validateBindings = validateBindings;
|
|
35
|
+
exports.cmdPlatformBindings = cmdPlatformBindings;
|
|
36
|
+
exports.cmdValidateBindings = cmdValidateBindings;
|
|
37
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const io = require("./io.cjs");
|
|
41
|
+
const { output, error, ERROR_REASON } = io;
|
|
42
|
+
const project_root_cjs_1 = require("./project-root.cjs");
|
|
43
|
+
const semver_compare_cjs_1 = require("./semver-compare.cjs");
|
|
44
|
+
// Core load-bearing patterns (D-01). These MUST mirror the `pattern` fields in
|
|
45
|
+
// schemas/platform-bindings.schema.json — the coupling test fails if they drift.
|
|
46
|
+
const SCHEMA_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
47
|
+
const PLATFORM_REPO_RE = /^[\w.-]+\/[\w.-]+$/;
|
|
48
|
+
const PLATFORM_TAG_RE = /^v\d+\.\d+\.\d+/;
|
|
49
|
+
// Provisional namespace pattern (lenient): only enforced if client.slug is present.
|
|
50
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
51
|
+
function isObject(v) {
|
|
52
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Hand-rolled validator (no ajv). Hard-checks ONLY the frozen core per D-03;
|
|
56
|
+
* provisional namespaces are validated leniently. Returns the verdict as data.
|
|
57
|
+
*
|
|
58
|
+
* @param o - The parsed binding object (untrusted shape).
|
|
59
|
+
* @returns `{ valid, errors }` — `errors` names each failing core field.
|
|
60
|
+
*/
|
|
61
|
+
function validateBindings(o) {
|
|
62
|
+
const errors = [];
|
|
63
|
+
if (!isObject(o)) {
|
|
64
|
+
return { valid: false, errors: ['root: expected a JSON object'] };
|
|
65
|
+
}
|
|
66
|
+
// schemaVersion — required, semver triplet.
|
|
67
|
+
if (typeof o.schemaVersion !== 'string' || !SCHEMA_VERSION_RE.test(o.schemaVersion)) {
|
|
68
|
+
errors.push('schemaVersion: missing or not a semver triplet (e.g. "1.0.0")');
|
|
69
|
+
}
|
|
70
|
+
// platform — required object with repo + tag.
|
|
71
|
+
if (!isObject(o.platform)) {
|
|
72
|
+
errors.push('platform: missing or not an object');
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const platform = o.platform;
|
|
76
|
+
if (typeof platform.repo !== 'string' || !PLATFORM_REPO_RE.test(platform.repo)) {
|
|
77
|
+
errors.push('platform.repo: missing or not a "group/project" path');
|
|
78
|
+
}
|
|
79
|
+
if (typeof platform.tag !== 'string' || !PLATFORM_TAG_RE.test(platform.tag)) {
|
|
80
|
+
errors.push('platform.tag: missing or not a "v<semver>" tag');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Provisional namespaces — lenient. A missing/unknown namespace is fine; only
|
|
84
|
+
// a PRESENT slug field that is malformed is flagged (it never gates on absence).
|
|
85
|
+
if (isObject(o.client) && o.client.slug !== undefined) {
|
|
86
|
+
if (typeof o.client.slug !== 'string' || !SLUG_RE.test(o.client.slug)) {
|
|
87
|
+
errors.push('client.slug: present but not a url-safe lowercase slug');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// IN-03: project.slug — symmetric lenient check matching client.slug behaviour.
|
|
91
|
+
// The schema defines the same ^[a-z0-9][a-z0-9-]*$ pattern for both fields.
|
|
92
|
+
if (isObject(o.project) && o.project.slug !== undefined) {
|
|
93
|
+
if (typeof o.project.slug !== 'string' || !SLUG_RE.test(o.project.slug)) {
|
|
94
|
+
errors.push('project.slug: present but not a url-safe lowercase slug');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { valid: errors.length === 0, errors };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reader handler for `pp-tools query platform-bindings`.
|
|
101
|
+
*
|
|
102
|
+
* Behaviour matrix (locked by tests/platform-bindings.test.cjs):
|
|
103
|
+
* absent file → output({present:false}) exit 0 (inert)
|
|
104
|
+
* present + valid JSON → output({present:true, ...bindings}) exit 0
|
|
105
|
+
* present + --validate → output(validateBindings(obj)) exit 0
|
|
106
|
+
* malformed JSON → error(..., PARSE_FAILED) exit 1
|
|
107
|
+
*
|
|
108
|
+
* @param cwd - Working directory; resolved to repo root via findProjectRoot.
|
|
109
|
+
* @param args - Handler-local args (already sliced past the subcommand name).
|
|
110
|
+
* @param raw - Raw output mode (passed through to output()).
|
|
111
|
+
*/
|
|
112
|
+
function cmdPlatformBindings(cwd, args, raw) {
|
|
113
|
+
let root;
|
|
114
|
+
try {
|
|
115
|
+
root = (0, project_root_cjs_1.findProjectRoot)(cwd) || cwd;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
root = cwd;
|
|
119
|
+
}
|
|
120
|
+
const bindingsPath = node_path_1.default.join(root, '.platform-bindings.json');
|
|
121
|
+
// Absent → inert. NEVER error() here; this is the opt-in default path.
|
|
122
|
+
if (!node_fs_1.default.existsSync(bindingsPath)) {
|
|
123
|
+
output({ present: false }, raw);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(bindingsPath, 'utf-8'));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// IN-02: TOCTOU guard — if the file was deleted between existsSync and readFileSync,
|
|
132
|
+
// route to the inert absent path rather than emitting a misleading parse-failure error.
|
|
133
|
+
if (err.code === 'ENOENT') {
|
|
134
|
+
output({ present: false }, raw);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
error('Failed to parse .platform-bindings.json: ' + err.message, ERROR_REASON.PLATFORM_BINDINGS_PARSE_FAILED);
|
|
138
|
+
return; // unreachable — error() exits; kept for type-narrowing clarity.
|
|
139
|
+
}
|
|
140
|
+
if (args.includes('--validate')) {
|
|
141
|
+
output(validateBindings(parsed), raw);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// present + valid → echo the bindings under a present flag.
|
|
145
|
+
output(Object.assign({ present: true }, parsed), raw);
|
|
146
|
+
}
|
|
147
|
+
// Anchored regex for `?ref=` values in module source lines (T-07-04: untrusted text,
|
|
148
|
+
// no YAML dependency, no shell-eval).
|
|
149
|
+
const GITLAB_CI_MODULE_REF_RE = /\?ref=([^\s'"&)\]]+)/g;
|
|
150
|
+
/**
|
|
151
|
+
* Extracts `ref:` values that belong to the top-level `include:` block in a
|
|
152
|
+
* .gitlab-ci.yml file, using a stateful line-scan.
|
|
153
|
+
*
|
|
154
|
+
* APPROXIMATION: Parses YAML structure via indentation tracking — handles the
|
|
155
|
+
* common block-sequence form (`include: [- project: ... ref: ...]`). It does NOT
|
|
156
|
+
* capture `ref:` values from `trigger:`, `needs:`, or other non-include stanzas,
|
|
157
|
+
* because those appear under job-level keys (indent = 0) which close the include
|
|
158
|
+
* context. May mis-classify exotic YAML (flow mappings, anchors, multi-document
|
|
159
|
+
* files). Uses no YAML parser to avoid runtime dependencies (T-07-04).
|
|
160
|
+
*
|
|
161
|
+
* @param content - Raw string content of the .gitlab-ci.yml file.
|
|
162
|
+
* @returns Array of `ref:` values found inside the `include:` block.
|
|
163
|
+
*/
|
|
164
|
+
function extractGitlabCiIncludeRefs(content) {
|
|
165
|
+
const refs = [];
|
|
166
|
+
const lines = content.split('\n');
|
|
167
|
+
let inIncludeBlock = false;
|
|
168
|
+
const REF_LINE_RE = /^ref:\s+['"]?([^\s'"#,\]]+)['"]?/;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const trimmed = line.trimStart();
|
|
171
|
+
// Skip blank lines and comment lines — they do not change YAML context.
|
|
172
|
+
if (trimmed.length === 0 || trimmed.startsWith('#'))
|
|
173
|
+
continue;
|
|
174
|
+
const indent = line.length - trimmed.length;
|
|
175
|
+
// A new top-level key (indent = 0) either enters or exits the include block.
|
|
176
|
+
if (indent === 0) {
|
|
177
|
+
inIncludeBlock = trimmed === 'include:' || trimmed.startsWith('include:');
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Within the include block, capture any `ref: <value>` line.
|
|
181
|
+
if (inIncludeBlock) {
|
|
182
|
+
const m = trimmed.match(REF_LINE_RE);
|
|
183
|
+
if (m) {
|
|
184
|
+
refs.push(m[1].trim());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return refs;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pin-site consistency check for `pp-tools query validate-bindings` (BIND-03).
|
|
192
|
+
*
|
|
193
|
+
* Reads the REAL on-disk `.gitlab-ci.yml` include `ref:` and module `?ref=` values (D-06),
|
|
194
|
+
* cross-checks them against `platform.tag` in `.platform-bindings.json`.
|
|
195
|
+
*
|
|
196
|
+
* Behaviour matrix (locked by tests/platform-bindings.test.cjs):
|
|
197
|
+
* absent .platform-bindings.json → output({present:false}) exit 0 (inert, D-07)
|
|
198
|
+
* internal mismatch (>1 distinct) → output({..., status:'block'}) exit 0 (D-04)
|
|
199
|
+
* all-agree, behind --latest-tag → output({..., status:'warn'}) exit 0 (D-05)
|
|
200
|
+
* all-agree, at/ahead of latest → output({..., status:'pass'}) exit 0
|
|
201
|
+
*
|
|
202
|
+
* Threat mitigations:
|
|
203
|
+
* T-07-04 Anchored regex on `ref:`/`?ref=`; file content is untrusted text — never shell-eval.
|
|
204
|
+
* T-07-05 Unresolved/offline remote is a soft note, never a hard block; caller passes
|
|
205
|
+
* `--latest-tag <tag>` when a network lookup succeeded; absent = skip drift check.
|
|
206
|
+
* T-07-06 `compareSemverCore === 0` normalises v-prefix differences; literal diff surfaced in `sites`.
|
|
207
|
+
*
|
|
208
|
+
* @param cwd - Working directory; resolved to repo root via findProjectRoot.
|
|
209
|
+
* @param args - Handler-local args (already sliced past the subcommand name).
|
|
210
|
+
* Accepts optional `--latest-tag <tag>` for D-05 drift classification.
|
|
211
|
+
* @param raw - Raw output mode (passed through to output()).
|
|
212
|
+
*/
|
|
213
|
+
function cmdValidateBindings(cwd, args, raw) {
|
|
214
|
+
let root;
|
|
215
|
+
try {
|
|
216
|
+
root = (0, project_root_cjs_1.findProjectRoot)(cwd) || cwd;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
root = cwd;
|
|
220
|
+
}
|
|
221
|
+
const bindingsPath = node_path_1.default.join(root, '.platform-bindings.json');
|
|
222
|
+
// D-07: absent → inert. NEVER error() here; this is the opt-in default path.
|
|
223
|
+
if (!node_fs_1.default.existsSync(bindingsPath)) {
|
|
224
|
+
output({ present: false }, raw);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Parse the bindings file (malformed → hard error, same rule as the reader).
|
|
228
|
+
let parsed;
|
|
229
|
+
try {
|
|
230
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(bindingsPath, 'utf-8'));
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
// IN-02: TOCTOU guard — if the file was deleted between existsSync and readFileSync,
|
|
234
|
+
// route to the inert absent path rather than emitting a misleading parse-failure error.
|
|
235
|
+
if (err.code === 'ENOENT') {
|
|
236
|
+
output({ present: false }, raw);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
error('Failed to parse .platform-bindings.json: ' + err.message, ERROR_REASON.PLATFORM_BINDINGS_PARSE_FAILED);
|
|
240
|
+
return; // unreachable — error() exits; kept for type-narrowing clarity.
|
|
241
|
+
}
|
|
242
|
+
// Extract canonical platform.tag (T) as the first pin-site value.
|
|
243
|
+
const bindingsObj = parsed;
|
|
244
|
+
const platform = isObject(bindingsObj.platform) ? bindingsObj.platform : null;
|
|
245
|
+
const bindingsTag = typeof platform?.tag === 'string' ? platform.tag.trim() : '';
|
|
246
|
+
// WR-01: guard on a missing/empty canonical tag — without it, the function
|
|
247
|
+
// would fall through to emit status:'pass' with zero pin-sites, misleadingly
|
|
248
|
+
// signalling a correct pin on a binding that validateBindings would reject.
|
|
249
|
+
const sites = {};
|
|
250
|
+
if (!bindingsTag) {
|
|
251
|
+
output({
|
|
252
|
+
present: true,
|
|
253
|
+
consistent: false,
|
|
254
|
+
status: 'block',
|
|
255
|
+
message: 'platform.tag is missing or empty in .platform-bindings.json — run --validate to diagnose',
|
|
256
|
+
sites,
|
|
257
|
+
}, raw);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
sites['platform.tag'] = bindingsTag;
|
|
261
|
+
// D-06: read the REAL on-disk .gitlab-ci.yml and extract ref values.
|
|
262
|
+
const gitlabCiPath = node_path_1.default.join(root, '.gitlab-ci.yml');
|
|
263
|
+
if (node_fs_1.default.existsSync(gitlabCiPath)) {
|
|
264
|
+
const ciContent = node_fs_1.default.readFileSync(gitlabCiPath, 'utf-8');
|
|
265
|
+
// Extract `ref:` values from include blocks using a stateful line-scan (WR-02).
|
|
266
|
+
// Avoids false positives from `trigger:`, `needs:`, and other non-include stanzas.
|
|
267
|
+
const includeRefs = extractGitlabCiIncludeRefs(ciContent);
|
|
268
|
+
for (let refIdx = 0; refIdx < includeRefs.length; refIdx++) {
|
|
269
|
+
const key = refIdx === 0
|
|
270
|
+
? '.gitlab-ci.yml include ref'
|
|
271
|
+
: `.gitlab-ci.yml include ref[${refIdx + 1}]`;
|
|
272
|
+
sites[key] = includeRefs[refIdx];
|
|
273
|
+
}
|
|
274
|
+
// Extract `?ref=` values from module source lines.
|
|
275
|
+
const moduleRefRe = new RegExp(GITLAB_CI_MODULE_REF_RE.source, GITLAB_CI_MODULE_REF_RE.flags);
|
|
276
|
+
let m;
|
|
277
|
+
let modIdx = 0;
|
|
278
|
+
while ((m = moduleRefRe.exec(ciContent)) !== null) {
|
|
279
|
+
modIdx++;
|
|
280
|
+
const key = modIdx === 1
|
|
281
|
+
? '.gitlab-ci.yml module ?ref'
|
|
282
|
+
: `.gitlab-ci.yml module ?ref[${modIdx}]`;
|
|
283
|
+
sites[key] = m[1].trim();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Build distinct semver set across all pin-sites.
|
|
287
|
+
// `compareSemverCore === 0` normalises v-prefix differences (T-07-06).
|
|
288
|
+
const tagValues = Object.values(sites).filter(Boolean);
|
|
289
|
+
const distinctTags = [];
|
|
290
|
+
for (const tag of tagValues) {
|
|
291
|
+
const isDuplicate = distinctTags.some((existing) => (0, semver_compare_cjs_1.compareSemverCore)(tag, existing) === 0);
|
|
292
|
+
if (!isDuplicate) {
|
|
293
|
+
distinctTags.push(tag);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// D-04: hard BLOCK when >1 semver-distinct element across the pin-sites.
|
|
297
|
+
if (distinctTags.length > 1) {
|
|
298
|
+
const siteDiff = Object.entries(sites)
|
|
299
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
300
|
+
.join(', ');
|
|
301
|
+
output({
|
|
302
|
+
present: true,
|
|
303
|
+
consistent: false,
|
|
304
|
+
status: 'block',
|
|
305
|
+
message: `Pin-site tags disagree: ${siteDiff}`,
|
|
306
|
+
sites,
|
|
307
|
+
}, raw);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// D-05: soft WARN when all sites agree but are behind the latest stable tag.
|
|
311
|
+
// The caller passes `--latest-tag <tag>` when a network ls-remote succeeded.
|
|
312
|
+
// Absent or unresolved remote → skip drift check (T-07-05: never hard-block).
|
|
313
|
+
const latestTagArgIdx = args.indexOf('--latest-tag');
|
|
314
|
+
const latestTag = latestTagArgIdx >= 0 ? args[latestTagArgIdx + 1] : undefined;
|
|
315
|
+
if (latestTag && (0, semver_compare_cjs_1.isStableTripletSemver)(latestTag) && bindingsTag) {
|
|
316
|
+
const drift = (0, semver_compare_cjs_1.compareSemverCore)(bindingsTag, latestTag);
|
|
317
|
+
if (drift < 0) {
|
|
318
|
+
// Strictly behind latest stable.
|
|
319
|
+
output({
|
|
320
|
+
present: true,
|
|
321
|
+
consistent: true,
|
|
322
|
+
status: 'warn',
|
|
323
|
+
message: `All pin-sites agree on ${bindingsTag}, but latest stable is ${latestTag}`,
|
|
324
|
+
sites,
|
|
325
|
+
}, raw);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// All sites agree, not strictly behind latest → pass.
|
|
330
|
+
output({
|
|
331
|
+
present: true,
|
|
332
|
+
consistent: true,
|
|
333
|
+
status: 'pass',
|
|
334
|
+
sites,
|
|
335
|
+
}, raw);
|
|
336
|
+
}
|
|
@@ -1061,13 +1061,14 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1061
1061
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.dirname(outputPath));
|
|
1062
1062
|
(0, shell_command_projection_cjs_1.platformWriteSync)(outputPath, existingContent);
|
|
1063
1063
|
}
|
|
1064
|
-
else if (!/<!-- PP:[a-z]
|
|
1064
|
+
else if (!/<!-- PP:[a-z][a-z-]*-start/.test(existingContent) && !options.force) {
|
|
1065
1065
|
// #1098: the target instruction file already exists and contains NO GSD
|
|
1066
1066
|
// section markers — it is a hand-crafted CLAUDE.md/AGENTS.md, not a
|
|
1067
1067
|
// GSD-managed one. Do NOT clobber it with generated project documentation
|
|
1068
1068
|
// (broad project detail belongs in PROJECT.md / REQUIREMENTS.md, which GSD
|
|
1069
1069
|
// already owns). Leave the file untouched and report a skip; `--force`
|
|
1070
1070
|
// overwrites intentionally.
|
|
1071
|
+
// Note: [a-z][a-z-]* matches section names with hyphens (e.g. client-context)
|
|
1071
1072
|
output({
|
|
1072
1073
|
claude_md_path: outputPath,
|
|
1073
1074
|
action: 'skipped',
|
|
@@ -1153,12 +1154,226 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1153
1154
|
};
|
|
1154
1155
|
output(result, raw, undefined);
|
|
1155
1156
|
}
|
|
1157
|
+
// ─── Client Context Block ─────────────────────────────────────────────────────
|
|
1158
|
+
/**
|
|
1159
|
+
* Sanitize a field value so pasted content cannot break the managed block.
|
|
1160
|
+
* T-09-01: Neutralizes:
|
|
1161
|
+
* - any `<!-- PP:` marker sequence (any section name — start or end fragment)
|
|
1162
|
+
* - any line starting with `### ` (would forge a field subheading)
|
|
1163
|
+
*
|
|
1164
|
+
* WR-01: Broadened from `PP:client-context` to `PP:[a-z][a-z-]*` so that a
|
|
1165
|
+
* pasted value containing e.g. `<!-- PP:project-end -->` cannot corrupt another
|
|
1166
|
+
* section's `updateSection` / `generate-claude-md` call later.
|
|
1167
|
+
*
|
|
1168
|
+
* Neutralization strategy for `<!-- PP:` sequences: a zero-width space (U+200B)
|
|
1169
|
+
* is inserted INSIDE the comment opener (`<` + ZWS + `!--`) rather than prepended
|
|
1170
|
+
* before it. This breaks the literal substring `<!--` so that `String.indexOf` and
|
|
1171
|
+
* `String.includes` can no longer find `<!-- PP:...` in the neutralized value —
|
|
1172
|
+
* which is required because `extractSectionContent` / `updateSection` use
|
|
1173
|
+
* `fileContent.indexOf(endMarker)` to locate section boundaries. A ZWS *prefix*
|
|
1174
|
+
* before `<!--` (approach used by earlier code) still left `<!-- PP:...` as a
|
|
1175
|
+
* findable substring, allowing the injection to truncate block reads.
|
|
1176
|
+
*
|
|
1177
|
+
* The inserted ZWS is invisible in rendered text and does not match the replacement
|
|
1178
|
+
* pattern on a subsequent sanitization pass, so round-trip re-writes are idempotent.
|
|
1179
|
+
*
|
|
1180
|
+
* Neutralization for `### ` uses a ZWS *prefix* (before `### `), which is the
|
|
1181
|
+
* correct approach because `parseClientContextFields` checks for lines that EQUAL
|
|
1182
|
+
* the heading string exactly — `### Client` !== `### Client`.
|
|
1183
|
+
*/
|
|
1184
|
+
function sanitizeClientContextField(value) {
|
|
1185
|
+
// Neutralize any PP managed-block marker: break `<!--` by inserting ZWS inside it
|
|
1186
|
+
// so the literal substring `<!-- PP:...` no longer appears in the neutralized value.
|
|
1187
|
+
let result = value.replace(/(<!--)(\s*PP:[a-z][a-z-]*)/gi, '<!--$2');
|
|
1188
|
+
// Neutralize lines beginning with `### ` (subheading injection)
|
|
1189
|
+
result = result.replace(/^(### )/gm, '$1');
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Parse the five field values from an extracted client-context block body.
|
|
1194
|
+
* Uses `### ` anchor scan on the block content (T-09-03: never whole-file).
|
|
1195
|
+
* Returns an object with the five field keys, or null if the block is empty.
|
|
1196
|
+
*
|
|
1197
|
+
* CR-01: The `### Client` section is stored under `client_name_slug` for the
|
|
1198
|
+
* combined block, but `client_name` and `client_slug` are also extracted as
|
|
1199
|
+
* separate keys from the `**Name:**` / `**Slug:**` lines within that section
|
|
1200
|
+
* so that `init.md` edit-on-re-run can read them via `o.client_name` /
|
|
1201
|
+
* `o.client_slug` without breakage.
|
|
1202
|
+
*/
|
|
1203
|
+
function parseClientContextFields(blockContent) {
|
|
1204
|
+
const SUBHEADINGS = [
|
|
1205
|
+
{ key: 'client_name_slug', heading: '### Client' },
|
|
1206
|
+
{ key: 'coding_standards', heading: '### Coding Standards' },
|
|
1207
|
+
{ key: 'architecture_patterns', heading: '### Architecture Patterns' },
|
|
1208
|
+
{ key: 'ui_brand_conventions', heading: '### UI / Brand Conventions' },
|
|
1209
|
+
{ key: 'definition_of_done', heading: '### Definition of Done' },
|
|
1210
|
+
];
|
|
1211
|
+
const result = {};
|
|
1212
|
+
// WR-03: strip trailing \r so CRLF files don't break exact-equality heading matches
|
|
1213
|
+
const lines = blockContent.split('\n').map(l => l.replace(/\r$/, ''));
|
|
1214
|
+
for (let i = 0; i < SUBHEADINGS.length; i++) {
|
|
1215
|
+
const { key, heading } = SUBHEADINGS[i];
|
|
1216
|
+
const nextHeading = i + 1 < SUBHEADINGS.length ? SUBHEADINGS[i + 1].heading : null;
|
|
1217
|
+
const startIdx = lines.findIndex(l => l === heading);
|
|
1218
|
+
if (startIdx === -1) {
|
|
1219
|
+
result[key] = '';
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const sectionLines = [];
|
|
1223
|
+
for (let j = startIdx + 1; j < lines.length; j++) {
|
|
1224
|
+
if (nextHeading && lines[j] === nextHeading)
|
|
1225
|
+
break;
|
|
1226
|
+
if (!nextHeading && /^<!-- PP:client-context-end/.test(lines[j]))
|
|
1227
|
+
break;
|
|
1228
|
+
sectionLines.push(lines[j]);
|
|
1229
|
+
}
|
|
1230
|
+
result[key] = sectionLines.join('\n').trim();
|
|
1231
|
+
// CR-01: for the ### Client section, also extract client_name and client_slug
|
|
1232
|
+
// as separate keys by parsing the **Name:** / **Slug:** lines within it.
|
|
1233
|
+
if (key === 'client_name_slug') {
|
|
1234
|
+
const nameLine = sectionLines.find(l => l.startsWith('**Name:**'));
|
|
1235
|
+
result['client_name'] = nameLine ? nameLine.replace(/^\*\*Name:\*\*\s*/, '').trim() : '';
|
|
1236
|
+
const slugLine = sectionLines.find(l => l.startsWith('**Slug:**'));
|
|
1237
|
+
result['client_slug'] = slugLine ? slugLine.replace(/^\*\*Slug:\*\*\s*/, '').trim() : '';
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
// WR-02: return null when no recognized subheadings were found so the caller's
|
|
1241
|
+
// `if (!fields)` guard correctly emits { present: false } for empty/hand-edited blocks
|
|
1242
|
+
const hasAnyField = Object.values(result).some(v => v !== '');
|
|
1243
|
+
return hasAnyField ? result : null;
|
|
1244
|
+
}
|
|
1245
|
+
function cmdGenerateClientContext(cwd, options, raw) {
|
|
1246
|
+
// Resolve target path using same config-driven logic as cmdGenerateClaudeMd
|
|
1247
|
+
let configClaudeMdPath = './.claude/CLAUDE.md';
|
|
1248
|
+
let config = {};
|
|
1249
|
+
try {
|
|
1250
|
+
config = loadConfig(cwd);
|
|
1251
|
+
if (config['claude_md_path'])
|
|
1252
|
+
configClaudeMdPath = config['claude_md_path'];
|
|
1253
|
+
// Runtime-awareness: Codex projects write to AGENTS.md, not CLAUDE.md
|
|
1254
|
+
const effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime']);
|
|
1255
|
+
if (!options.output && effectiveRuntime === 'codex') {
|
|
1256
|
+
configClaudeMdPath = './AGENTS.md';
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch { /* use default */ }
|
|
1260
|
+
let targetPath;
|
|
1261
|
+
if (options.output) {
|
|
1262
|
+
targetPath = node_path_1.default.isAbsolute(options.output) ? options.output : node_path_1.default.join(cwd, options.output);
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
targetPath = node_path_1.default.isAbsolute(configClaudeMdPath) ? configClaudeMdPath : node_path_1.default.join(cwd, configClaudeMdPath);
|
|
1266
|
+
}
|
|
1267
|
+
// ── --read mode: parse existing block and return field values ───────────────
|
|
1268
|
+
if (options.read) {
|
|
1269
|
+
const fileContent = (0, shell_command_projection_cjs_1.platformReadSync)(targetPath);
|
|
1270
|
+
if (!fileContent) {
|
|
1271
|
+
output({ present: false }, raw, undefined);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const blockContent = extractSectionContent(fileContent, 'client-context');
|
|
1275
|
+
if (!blockContent) {
|
|
1276
|
+
output({ present: false }, raw, undefined);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const fields = parseClientContextFields(blockContent);
|
|
1280
|
+
if (!fields) {
|
|
1281
|
+
output({ present: false }, raw, undefined);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
output({ present: true, ...fields }, raw, undefined);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
// ── --from-json mode: render block and write ────────────────────────────────
|
|
1288
|
+
if (!options.fromJson) {
|
|
1289
|
+
error('--from-json <path> is required (or use --read to read existing block)');
|
|
1290
|
+
}
|
|
1291
|
+
let jsonPath = options.fromJson;
|
|
1292
|
+
if (!node_path_1.default.isAbsolute(jsonPath))
|
|
1293
|
+
jsonPath = node_path_1.default.join(cwd, jsonPath);
|
|
1294
|
+
if (!node_fs_1.default.existsSync(jsonPath))
|
|
1295
|
+
error(`JSON input file not found: ${jsonPath}`);
|
|
1296
|
+
let clientData;
|
|
1297
|
+
const jsonRaw = (0, shell_command_projection_cjs_1.platformReadSync)(jsonPath);
|
|
1298
|
+
try {
|
|
1299
|
+
if (jsonRaw === null)
|
|
1300
|
+
throw new Error(`JSON file not found: ${jsonPath}`);
|
|
1301
|
+
clientData = JSON.parse(jsonRaw);
|
|
1302
|
+
}
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
error(`Failed to parse JSON input: ${err.message}`);
|
|
1305
|
+
}
|
|
1306
|
+
const clientName = sanitizeClientContextField(clientData.client_name || '');
|
|
1307
|
+
const clientSlug = sanitizeClientContextField(clientData.client_slug || '');
|
|
1308
|
+
const codingStandards = sanitizeClientContextField(clientData.coding_standards || '');
|
|
1309
|
+
const archPatterns = sanitizeClientContextField(clientData.architecture_patterns || '');
|
|
1310
|
+
const uiBrand = sanitizeClientContextField(clientData.ui_brand_conventions || '');
|
|
1311
|
+
const dod = sanitizeClientContextField(clientData.definition_of_done || '');
|
|
1312
|
+
const sectionLines = [
|
|
1313
|
+
'<!-- PP:client-context-start -->',
|
|
1314
|
+
'## Client Context & Patterns',
|
|
1315
|
+
'',
|
|
1316
|
+
'When building or validating this project, apply these client patterns:',
|
|
1317
|
+
'',
|
|
1318
|
+
'### Client',
|
|
1319
|
+
'',
|
|
1320
|
+
`**Name:** ${clientName}`,
|
|
1321
|
+
`**Slug:** ${clientSlug}`,
|
|
1322
|
+
'',
|
|
1323
|
+
'### Coding Standards',
|
|
1324
|
+
'',
|
|
1325
|
+
codingStandards,
|
|
1326
|
+
'',
|
|
1327
|
+
'### Architecture Patterns',
|
|
1328
|
+
'',
|
|
1329
|
+
archPatterns,
|
|
1330
|
+
'',
|
|
1331
|
+
'### UI / Brand Conventions',
|
|
1332
|
+
'',
|
|
1333
|
+
uiBrand,
|
|
1334
|
+
'',
|
|
1335
|
+
'### Definition of Done',
|
|
1336
|
+
'',
|
|
1337
|
+
dod,
|
|
1338
|
+
'<!-- PP:client-context-end -->',
|
|
1339
|
+
];
|
|
1340
|
+
const sectionContent = sectionLines.join('\n');
|
|
1341
|
+
const existingContent = (0, shell_command_projection_cjs_1.platformReadSync)(targetPath);
|
|
1342
|
+
let action;
|
|
1343
|
+
if (existingContent === null) {
|
|
1344
|
+
// File does not exist: create it with the block
|
|
1345
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.dirname(targetPath));
|
|
1346
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(targetPath, sectionContent + '\n');
|
|
1347
|
+
action = 'created';
|
|
1348
|
+
}
|
|
1349
|
+
else if (!/<!-- PP:[a-z][a-z-]*-start/.test(existingContent) && !options.force) {
|
|
1350
|
+
// Hand-crafted guard (T-09-02): existing file has no PP markers → skip
|
|
1351
|
+
// Note: [a-z][a-z-]* matches section names with hyphens (e.g. client-context)
|
|
1352
|
+
output({
|
|
1353
|
+
action: 'skipped',
|
|
1354
|
+
claude_md_path: targetPath,
|
|
1355
|
+
reason: 'existing instruction file has no GSD markers (hand-crafted); not overwriting. Pass --force to overwrite.',
|
|
1356
|
+
}, raw, undefined);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
// Replace-or-append idempotently via updateSection
|
|
1361
|
+
const updated = updateSection(existingContent, 'client-context', sectionContent);
|
|
1362
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(targetPath, updated.content);
|
|
1363
|
+
action = updated.action;
|
|
1364
|
+
}
|
|
1365
|
+
output({ action, claude_md_path: targetPath }, raw, undefined);
|
|
1366
|
+
}
|
|
1156
1367
|
module.exports = {
|
|
1157
1368
|
cmdWriteProfile,
|
|
1158
1369
|
cmdProfileQuestionnaire,
|
|
1159
1370
|
cmdGenerateDevPreferences,
|
|
1160
1371
|
cmdGenerateClaudeProfile,
|
|
1161
1372
|
cmdGenerateClaudeMd,
|
|
1373
|
+
cmdGenerateClientContext,
|
|
1374
|
+
updateSection,
|
|
1375
|
+
extractSectionContent,
|
|
1376
|
+
extractMarkdownSection,
|
|
1162
1377
|
PROFILING_QUESTIONS,
|
|
1163
1378
|
CLAUDE_INSTRUCTIONS,
|
|
1164
1379
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* prohibition-enforcement — the deterministic PRODUCER for test-tier prohibition verification
|
|
4
|
-
* (#1259, ADR-550 Decision 5d "heavy half"; the D1 seam — a NEW deterministic
|
|
4
|
+
* (#1259, ADR-550 Decision 5d "heavy half"; the D1 seam — a NEW deterministic pp-tools
|
|
5
5
|
* sub-command, NOT free-form workflow prose).
|
|
6
6
|
*
|
|
7
7
|
* Today `dispositionForProhibition()` (src/probe-core.cts) already carries the POLICY seam: with
|
|
@@ -645,7 +645,7 @@ function parseRequest(args) {
|
|
|
645
645
|
}
|
|
646
646
|
}
|
|
647
647
|
/**
|
|
648
|
-
* CLI surface: `
|
|
648
|
+
* CLI surface: `pp_run check prohibition-enforcement <request.json>` (or `--json '<inline>'`).
|
|
649
649
|
* Parses the request, runs the producer, and emits the result as JSON. Honors the no-throw
|
|
650
650
|
* contract: malformed input -> structured `error(...)`, never an uncaught throw.
|
|
651
651
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Manifest-backed roadmap subcommand router.
|
|
4
|
-
* Keeps
|
|
4
|
+
* Keeps pp-tools.cjs thin while preserving existing command semantics.
|
|
5
5
|
*
|
|
6
6
|
* ADR-457 build-at-publish: the hand-written bin/lib/roadmap-command-router.cjs
|
|
7
7
|
* collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
|
@@ -40,7 +40,7 @@ function checkW021(content) {
|
|
|
40
40
|
const warnings = [];
|
|
41
41
|
// Sentinel milestone integers exempt from W021
|
|
42
42
|
const SENTINELS = new Set([0, 999]);
|
|
43
|
-
const MIGRATION_CMD = '
|
|
43
|
+
const MIGRATION_CMD = 'pp-tools roadmap upgrade --convention milestone-prefixed';
|
|
44
44
|
// Milestone section heading: ## [GSD] v2.0 — Label OR ## v2.0: Label OR ## Roadmap v2.0
|
|
45
45
|
// OR ## ✅ v2.0 OR ## 🚧 v2.0 (emoji-prefixed variants used by roadmap templates)
|
|
46
46
|
// Capture the major integer.
|
|
@@ -346,7 +346,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
346
346
|
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
|
|
347
347
|
console.warn('[gsd] Deprecated: free-form ROADMAP.md detected (no versioned milestone headings). ' +
|
|
348
348
|
'The project has phase_id_convention set to "milestone-prefixed" in config.json but the ' +
|
|
349
|
-
'ROADMAP does not use versioned milestone headings. Run `
|
|
349
|
+
'ROADMAP does not use versioned milestone headings. Run `pp-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default).');
|
|
350
350
|
}
|
|
351
351
|
if (versionOverride) {
|
|
352
352
|
const escapedVersion = escapeRegex(versionOverride);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Manifest-backed state subcommand router.
|
|
4
|
-
* Keeps
|
|
4
|
+
* Keeps pp-tools.cjs thin while preserving existing command semantics.
|
|
5
5
|
*
|
|
6
6
|
* Phase 5.1: handlers that have SDK equivalents are dispatched via
|
|
7
7
|
* executeForCjs (the sync bridge). CJS fallback is retained for:
|
|
@@ -50,7 +50,7 @@ function shouldResyncStateProgress(fields) {
|
|
|
50
50
|
// ─── Cache ────────────────────────────────────────────────────────────────────
|
|
51
51
|
// Cache disk scan results from buildStateFrontmatter per cwd per process (#1967).
|
|
52
52
|
// Avoids re-reading N+1 directories on every state write when the phase structure
|
|
53
|
-
// hasn't changed within the same
|
|
53
|
+
// hasn't changed within the same pp-tools invocation.
|
|
54
54
|
const _diskScanCache = new Map();
|
|
55
55
|
// Track all lock files held by this process so they can be removed on exit.
|
|
56
56
|
// process.on('exit') fires even on process.exit(1), unlike try/finally which is
|
|
@@ -250,7 +250,7 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
|
250
250
|
}
|
|
251
251
|
// Neither pattern matched — field may have been reformatted or removed.
|
|
252
252
|
// Log diagnostic so template drift is detected early rather than silently swallowed.
|
|
253
|
-
process.stderr.write(`[
|
|
253
|
+
process.stderr.write(`[pp-tools] WARNING: STATE.md field "${primary}"${fallback ? ` (fallback: "${fallback}")` : ''} not found — update skipped. ` +
|
|
254
254
|
`This may indicate STATE.md was externally modified or uses an unexpected format.\n`);
|
|
255
255
|
return content;
|
|
256
256
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Manifest-backed validate subcommand router.
|
|
4
|
-
* Keeps
|
|
4
|
+
* Keeps pp-tools.cjs thin while preserving existing command semantics.
|
|
5
5
|
*
|
|
6
6
|
* Phase 6: validate.consistency, validate.health, validate.agents are
|
|
7
7
|
* dispatched via executeForCjs when the SDK is available. CJS fallback
|