@pareto4/pp-core 1.11.1 → 1.12.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/README.md +31 -0
- package/agents/pp-code-fixer.md +1 -1
- package/agents/pp-debug-session-manager.md +1 -1
- package/agents/pp-debugger.md +1 -1
- package/agents/pp-executor.md +20 -10
- package/agents/pp-intel-updater.md +1 -1
- package/agents/pp-phase-researcher.md +1 -1
- package/agents/pp-plan-checker.md +5 -5
- package/agents/pp-planner.md +10 -24
- package/agents/pp-project-researcher.md +1 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-researcher.md +1 -1
- package/agents/pp-verifier.md +8 -4
- package/bin/install.js +429 -82
- package/commands/pp-autonomous.md +8 -6
- package/commands/pp-discovery-to-build.md +1 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-discuss-phase.md +2 -2
- package/commands/pp-execute-phase.md +3 -1
- package/commands/pp-graphify.md +5 -5
- package/commands/pp-import.md +1 -1
- package/commands/pp-map-codebase.md +1 -1
- package/commands/pp-ns-project.md +2 -3
- package/commands/pp-ns-workflow.md +1 -1
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-verify-work.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-cursor-session-start.js +2 -2
- package/hooks/dist/pp-session-state.sh +1 -1
- package/hooks/dist/pp-statusline.js +21 -2
- package/hooks/pp-cursor-session-start.js +2 -2
- package/hooks/pp-session-state.sh +1 -1
- package/hooks/pp-statusline.js +21 -2
- package/package.json +18 -5
- package/pareto-skills/pp-project-setup/SKILL.md +5 -63
- package/pp-core/bin/detect-build-commands.sh +182 -0
- package/pp-core/bin/lib/capability-activation.cjs +19 -9
- package/pp-core/bin/lib/capability-registry.cjs +65 -7
- package/pp-core/bin/lib/check-command-router.cjs +79 -38
- package/pp-core/bin/lib/clusters.cjs +2 -3
- package/pp-core/bin/lib/command-aliases.cjs +36 -0
- package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
- package/pp-core/bin/lib/commands.cjs +21 -6
- package/pp-core/bin/lib/compose-bindings.cjs +70 -6
- package/pp-core/bin/lib/config-loader.cjs +151 -8
- package/pp-core/bin/lib/config-schema.cjs +16 -0
- package/pp-core/bin/lib/config.cjs +142 -27
- package/pp-core/bin/lib/core-utils.cjs +43 -7
- package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
- package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
- package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
- package/pp-core/bin/lib/discovery-table.cjs +98 -0
- package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
- package/pp-core/bin/lib/frontmatter.cjs +17 -2
- package/pp-core/bin/lib/init-command-router.cjs +8 -5
- package/pp-core/bin/lib/init-contract.cjs +93 -4
- package/pp-core/bin/lib/init.cjs +124 -134
- package/pp-core/bin/lib/install-profiles.cjs +5 -0
- package/pp-core/bin/lib/install-run.cjs +536 -0
- package/pp-core/bin/lib/io.cjs +15 -0
- package/pp-core/bin/lib/loop-resolver.cjs +40 -2
- package/pp-core/bin/lib/milestone.cjs +345 -48
- package/pp-core/bin/lib/phase-command-router.cjs +23 -1
- package/pp-core/bin/lib/phase-locator.cjs +15 -6
- package/pp-core/bin/lib/phase-scope.cjs +91 -0
- package/pp-core/bin/lib/phase-tier.cjs +467 -0
- package/pp-core/bin/lib/phase.cjs +247 -21
- package/pp-core/bin/lib/phases-command-router.cjs +2 -0
- package/pp-core/bin/lib/plan-graph.cjs +368 -0
- package/pp-core/bin/lib/planning-workspace.cjs +53 -0
- package/pp-core/bin/lib/platform-bindings.cjs +32 -2
- package/pp-core/bin/lib/profile-output.cjs +11 -3
- package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
- package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
- package/pp-core/bin/lib/roadmap.cjs +141 -39
- package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
- package/pp-core/bin/lib/state-command-router.cjs +82 -13
- package/pp-core/bin/lib/state-document.cjs +124 -7
- package/pp-core/bin/lib/state.cjs +784 -257
- package/pp-core/bin/lib/template.cjs +22 -6
- package/pp-core/bin/lib/vault-command-router.cjs +57 -6
- package/pp-core/bin/lib/vault-practices.cjs +6 -36
- package/pp-core/bin/lib/vault-write-back.cjs +20 -4
- package/pp-core/bin/lib/vault.cjs +592 -23
- package/pp-core/bin/lib/verify.cjs +53 -31
- package/pp-core/bin/lib/workstream.cjs +1 -1
- package/pp-core/bin/lib/worktree-safety.cjs +26 -15
- package/pp-core/bin/portable-timeout.sh +89 -0
- package/pp-core/bin/pp-tools.cjs +149 -9
- package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
- package/pp-core/bin/shared/config-schema.manifest.json +3 -0
- package/pp-core/references/autonomous-best-effort.md +461 -0
- package/pp-core/references/autonomous-lifecycle.md +146 -0
- package/pp-core/references/autonomous-smart-discuss.md +16 -12
- package/pp-core/references/checkpoints.md +1 -1
- package/pp-core/references/discovery-id-conventions.md +202 -0
- package/pp-core/references/discovery-rebaseline.md +254 -0
- package/pp-core/references/discovery-research-track.md +283 -0
- package/pp-core/references/discovery-traceability.md +400 -0
- package/pp-core/references/loop-hook-dispatch.md +14 -0
- package/pp-core/references/mandatory-initial-read.md +1 -1
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-human-verify-mode.md +1 -1
- package/pp-core/references/planner-load-graph-context.md +1 -1
- package/pp-core/references/planner-plan-graph.md +66 -0
- package/pp-core/references/planner-tdd.md +1 -0
- package/pp-core/references/planning-config.md +8 -0
- package/pp-core/references/sketch-tooling.md +4 -0
- package/pp-core/references/tdd.md +1 -0
- package/pp-core/templates/claude-md.md +1 -1
- package/pp-core/templates/phase-prompt.md +6 -2
- package/pp-core/templates/prototype-review/review.js +303 -0
- package/pp-core/templates/state.md +6 -0
- package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-phase.md +1 -1
- package/pp-core/workflows/add-tests.md +1 -1
- package/pp-core/workflows/add-todo.md +1 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -2
- package/pp-core/workflows/audit-fix.md +12 -16
- package/pp-core/workflows/audit-milestone.md +2 -2
- package/pp-core/workflows/audit-uat.md +1 -1
- package/pp-core/workflows/autonomous.md +68 -306
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/cleanup.md +1 -1
- package/pp-core/workflows/code-review-fix.md +2 -2
- package/pp-core/workflows/code-review.md +20 -8
- package/pp-core/workflows/complete-milestone.md +41 -60
- package/pp-core/workflows/debug.md +1 -1
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
- package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
- package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
- package/pp-core/workflows/discovery-sprint.md +79 -1714
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
- package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
- package/pp-core/workflows/discovery-to-build.md +21 -238
- package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
- package/pp-core/workflows/discuss-phase.md +11 -4
- package/pp-core/workflows/do.md +4 -4
- package/pp-core/workflows/docs-update.md +1 -1
- package/pp-core/workflows/edit-phase.md +1 -1
- package/pp-core/workflows/eval-review.md +1 -1
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
- package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
- package/pp-core/workflows/execute-phase.md +104 -218
- package/pp-core/workflows/execute-plan.md +6 -2
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/extract-learnings.md +1 -1
- package/pp-core/workflows/forensics.md +4 -4
- package/pp-core/workflows/graduation.md +1 -1
- package/pp-core/workflows/health.md +1 -1
- package/pp-core/workflows/help/modes/brief.md +1 -0
- package/pp-core/workflows/help/modes/default.md +2 -1
- package/pp-core/workflows/help/modes/full.md +12 -5
- package/pp-core/workflows/import.md +1 -1
- package/pp-core/workflows/ingest-docs.md +1 -1
- package/pp-core/workflows/init.md +18 -2
- package/pp-core/workflows/insert-phase.md +1 -1
- package/pp-core/workflows/list-workspaces.md +1 -1
- package/pp-core/workflows/manager.md +3 -3
- package/pp-core/workflows/map-codebase.md +2 -2
- package/pp-core/workflows/milestone-summary.md +4 -4
- package/pp-core/workflows/mvp-phase.md +1 -1
- package/pp-core/workflows/new-milestone.md +7 -8
- package/pp-core/workflows/new-project.md +6 -1
- package/pp-core/workflows/new-workspace.md +1 -1
- package/pp-core/workflows/next.md +2 -2
- package/pp-core/workflows/note.md +2 -2
- package/pp-core/workflows/pause-work.md +1 -1
- package/pp-core/workflows/plan-milestone-gaps.md +1 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
- package/pp-core/workflows/plan-phase.md +43 -167
- package/pp-core/workflows/plan-review-convergence.md +2 -2
- package/pp-core/workflows/plant-seed.md +1 -1
- package/pp-core/workflows/pr-branch.md +1 -1
- package/pp-core/workflows/profile-user.md +1 -1
- package/pp-core/workflows/progress.md +3 -3
- package/pp-core/workflows/quick.md +4 -2
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/remove-workspace.md +1 -1
- package/pp-core/workflows/resume-project.md +6 -12
- package/pp-core/workflows/review.md +1 -1
- package/pp-core/workflows/scan.md +1 -1
- package/pp-core/workflows/secure-phase.md +2 -2
- package/pp-core/workflows/settings-advanced.md +1 -1
- package/pp-core/workflows/settings-integrations.md +1 -1
- package/pp-core/workflows/settings.md +1 -1
- package/pp-core/workflows/ship.md +26 -9
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +2 -1
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +1 -1
- package/pp-core/workflows/stats.md +2 -2
- package/pp-core/workflows/thread.md +1 -1
- package/pp-core/workflows/transition.md +5 -9
- package/pp-core/workflows/ui-phase.md +23 -2
- package/pp-core/workflows/ui-review.md +1 -1
- package/pp-core/workflows/ultraplan-phase.md +2 -2
- package/pp-core/workflows/validate-phase.md +2 -2
- package/pp-core/workflows/vault-write-back.md +1 -1
- package/pp-core/workflows/verify-phase.md +29 -26
- package/pp-core/workflows/verify-work.md +14 -10
- package/scripts/base64-scan.sh +7 -41
- package/scripts/changeset/cli.cjs +25 -5
- package/scripts/dev-env.cjs +462 -0
- package/scripts/lib/allowlist-ratchet.cjs +103 -1
- package/scripts/lib/size-budget.cjs +311 -0
- package/scripts/lint-neutrality-names.cjs +694 -0
- package/scripts/lint-org-coordinates.allowlist.json +35 -0
- package/scripts/lint-org-coordinates.cjs +426 -0
- package/scripts/lint-skill-deps.cjs +48 -9
- package/scripts/lint-test-file-count.cjs +17 -2
- package/scripts/prompt-injection-scan.sh +41 -20
- package/scripts/sync-manifest-versions.cjs +41 -7
- package/scripts/sync-runtime-launcher.cjs +106 -1
- package/scripts/update-size-baseline.cjs +274 -30
- package/scripts/workflow-size.cjs +61 -9
- package/commands/pp-build-handoff.md +0 -46
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
|
@@ -43,6 +43,9 @@ const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
44
|
const federatedConfigModule = require("./federated-config.cjs");
|
|
45
45
|
const { mergeFederatedConfig } = federatedConfigModule;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const capabilityActivationModule = require("./capability-activation.cjs");
|
|
48
|
+
const { activationConfigFilesFor } = capabilityActivationModule;
|
|
46
49
|
// The capability-registry.cjs is generated and lives in the same gsd-core/bin/lib/ output dir.
|
|
47
50
|
// Both config-loader.cjs and capability-registry.cjs land in gsd-core/bin/lib/ at build time.
|
|
48
51
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
@@ -341,16 +344,154 @@ function _applyFederatedOverlay(baseConfig, userConfig) {
|
|
|
341
344
|
_applyFederatedValues(cloned, _fedOverlay.values, _fedOverlay.validKeys);
|
|
342
345
|
return cloned;
|
|
343
346
|
}
|
|
344
|
-
|
|
347
|
+
/**
|
|
348
|
+
* The workstream `loadConfig` reads for `options`: an explicit `workstream`
|
|
349
|
+
* option, then `workstreamContext.ws`, then `GSD_WORKSTREAM`. Shared with
|
|
350
|
+
* `configFilesFor` so the two can never resolve different workstreams.
|
|
351
|
+
*/
|
|
352
|
+
function _activeWorkstreamFor(options) {
|
|
345
353
|
const activeWorkstream = Object.prototype.hasOwnProperty.call(options, 'workstream')
|
|
346
354
|
? options['workstream']
|
|
347
355
|
: (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
|
|
348
356
|
? options['workstreamContext']['ws']
|
|
349
357
|
: (process.env['GSD_WORKSTREAM'] || null);
|
|
358
|
+
return typeof activeWorkstream === 'string' ? activeWorkstream : null;
|
|
359
|
+
}
|
|
360
|
+
function _configPathsFor(cwd, ws) {
|
|
361
|
+
return {
|
|
362
|
+
rootConfigPath: ws ? node_path_1.default.join(planningRoot(cwd), 'config.json') : null,
|
|
363
|
+
configPath: node_path_1.default.join(planningDir(cwd, ws), 'config.json'),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** `~/.gsd/defaults.json` (or under `GSD_HOME`): read only in a pre-project context (#1683). */
|
|
367
|
+
function _globalDefaultsPath() {
|
|
368
|
+
return node_path_1.default.join(process.env['GSD_HOME'] || node_os_1.default.homedir(), '.gsd', 'defaults.json');
|
|
369
|
+
}
|
|
370
|
+
/** The #1683 predicate: the global defaults are consulted only when the active planning dir is absent. */
|
|
371
|
+
function _readsGlobalDefaults(cwd, ws) {
|
|
372
|
+
return !node_fs_1.default.existsSync(planningDir(cwd, ws));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Every config file `loadConfig(cwd, options)` reads, as absolute paths in read
|
|
376
|
+
* order. ONE owner of "which config files does the loader read": `loadConfig`
|
|
377
|
+
* takes its paths from the same `_configPathsFor`, so a reader that must refuse
|
|
378
|
+
* an unreadable config (`unreadableConfigFiles` below) parses exactly these
|
|
379
|
+
* files without restating the workstream ladder.
|
|
380
|
+
*
|
|
381
|
+
* Why a caller needs it: `loadConfig` swallows a parse failure in every one of
|
|
382
|
+
* these files (root → "workstream config stands alone", active → the defaults),
|
|
383
|
+
* so its RESULT cannot say whether it read the project's config or read
|
|
384
|
+
* nothing. Files that do not exist are still listed; absence is not a failure
|
|
385
|
+
* for `loadConfig`, and the caller decides what it means.
|
|
386
|
+
*/
|
|
387
|
+
function configFilesFor(cwd, options = {}) {
|
|
388
|
+
const ws = _activeWorkstreamFor(options);
|
|
389
|
+
const { rootConfigPath, configPath } = _configPathsFor(cwd, ws);
|
|
390
|
+
const files = [];
|
|
391
|
+
if (rootConfigPath)
|
|
392
|
+
files.push(rootConfigPath);
|
|
393
|
+
files.push(configPath);
|
|
394
|
+
if (_readsGlobalDefaults(cwd, ws))
|
|
395
|
+
files.push(_globalDefaultsPath());
|
|
396
|
+
return files;
|
|
397
|
+
}
|
|
398
|
+
/** The error text is bounded; composed text never reaches the cap, but it holds anyway. */
|
|
399
|
+
const UNREADABLE_ERROR_CAP = 200;
|
|
400
|
+
/**
|
|
401
|
+
* Describe why a config file could not be used WITHOUT echoing it. V8's
|
|
402
|
+
* `JSON.parse` messages sometimes quote the input (`Unexpected token '}',
|
|
403
|
+
* "{"a":tru}" is not valid JSON`), so only the position is carried over.
|
|
404
|
+
*/
|
|
405
|
+
function _describeConfigReadFailure(err) {
|
|
406
|
+
const e = err;
|
|
407
|
+
if (e && typeof e.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(e.code))
|
|
408
|
+
return `cannot be read (${e.code})`;
|
|
409
|
+
const message = e && typeof e.message === 'string' ? e.message : '';
|
|
410
|
+
const lineCol = /\(line (\d+) column (\d+)\)/.exec(message);
|
|
411
|
+
if (lineCol)
|
|
412
|
+
return `not valid JSON (line ${lineCol[1]}, column ${lineCol[2]})`;
|
|
413
|
+
const position = /at position (\d+)/.exec(message);
|
|
414
|
+
if (position)
|
|
415
|
+
return `not valid JSON (at position ${position[1]})`;
|
|
416
|
+
if (/Unexpected end of JSON input/.test(message))
|
|
417
|
+
return 'not valid JSON (the text ends before the JSON does)';
|
|
418
|
+
return 'not valid JSON';
|
|
419
|
+
}
|
|
420
|
+
/** How a refusal names a config file: never an absolute user path. */
|
|
421
|
+
function _configDisplayPath(cwd, abs) {
|
|
422
|
+
const under = (base) => {
|
|
423
|
+
const rel = node_path_1.default.relative(base, abs);
|
|
424
|
+
return rel !== '' && !rel.startsWith('..') && !node_path_1.default.isAbsolute(rel) ? rel.split(node_path_1.default.sep).join('/') : null;
|
|
425
|
+
};
|
|
426
|
+
const inProject = under(cwd);
|
|
427
|
+
if (inProject !== null)
|
|
428
|
+
return inProject;
|
|
429
|
+
const homeOverride = process.env['GSD_HOME'];
|
|
430
|
+
const inHomeOverride = homeOverride ? under(homeOverride) : null;
|
|
431
|
+
if (inHomeOverride !== null)
|
|
432
|
+
return `$GSD_HOME/${inHomeOverride}`;
|
|
433
|
+
const inHome = under(node_os_1.default.homedir());
|
|
434
|
+
if (inHome !== null)
|
|
435
|
+
return `~/${inHome}`;
|
|
436
|
+
return node_path_1.default.basename(abs);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Every config file the hook resolver would read that EXISTS but cannot be
|
|
440
|
+
* used: unreadable, not JSON, or JSON that is not an object (`loadConfig`
|
|
441
|
+
* reads none of an array's or a null's keys and answers the defaults, the same
|
|
442
|
+
* silent fallback as a parse failure). Empty when every file is usable or
|
|
443
|
+
* absent: a MISSING config is a fresh project and reads as the defaults.
|
|
444
|
+
*
|
|
445
|
+
* The files are the union, in first-seen order, of the loader's list
|
|
446
|
+
* (`configFilesFor`) and the activation resolver's raw levels
|
|
447
|
+
* (`activationConfigFilesFor`, which can name a root config the loader never
|
|
448
|
+
* opens under `GSD_PROJECT`). Reads go through `platformReadSync`, the reader
|
|
449
|
+
* `loadConfig` uses, so a file the loader can read is never called unreadable.
|
|
450
|
+
*
|
|
451
|
+
* This is the ONE owner of "can the project's config be read". Both swallowing
|
|
452
|
+
* readers (`loadConfig`, `resolveConfigKey`) stay tolerant for their many
|
|
453
|
+
* callers; a caller whose answer governs a gate (`loop render-hooks`, the vault
|
|
454
|
+
* load verdict) asks this first and refuses. Never throws.
|
|
455
|
+
*/
|
|
456
|
+
function unreadableConfigFiles(cwd, options = {}) {
|
|
457
|
+
const out = [];
|
|
458
|
+
let files;
|
|
459
|
+
try {
|
|
460
|
+
files = [...new Set([...configFilesFor(cwd, options), ...activationConfigFilesFor(cwd)])];
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
// An invalid GSD_WORKSTREAM / GSD_PROJECT segment: the config cannot be located, so it cannot be read.
|
|
464
|
+
return [{ file: '.planning/config.json', error: `cannot be located (${err instanceof Error ? err.message : String(err)})`.slice(0, UNREADABLE_ERROR_CAP) }];
|
|
465
|
+
}
|
|
466
|
+
for (const file of files) {
|
|
467
|
+
let error = null;
|
|
468
|
+
try {
|
|
469
|
+
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(file);
|
|
470
|
+
if (raw === null)
|
|
471
|
+
continue;
|
|
472
|
+
const parsed = JSON.parse(raw);
|
|
473
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
474
|
+
error = 'valid JSON but not an object, so none of its settings can be read';
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
error = _describeConfigReadFailure(err);
|
|
479
|
+
}
|
|
480
|
+
if (error !== null)
|
|
481
|
+
out.push({ file: _configDisplayPath(cwd, file), error: error.slice(0, UNREADABLE_ERROR_CAP) });
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
/** One line per unreadable file, `<file>: <error>`, for a refusal message. */
|
|
486
|
+
function describeUnreadableConfig(unreadable) {
|
|
487
|
+
return unreadable.map((u) => `${u.file}: ${u.error}`).join('; ');
|
|
488
|
+
}
|
|
489
|
+
function loadConfig(cwd, options = {}) {
|
|
350
490
|
// When GSD_WORKSTREAM is set, load root config first so workstream config
|
|
351
491
|
// can inherit from it. This prevents users from duplicating model_overrides,
|
|
352
492
|
// workflow.*, etc. across every workstream config (#2714).
|
|
353
|
-
const ws =
|
|
493
|
+
const ws = _activeWorkstreamFor(options);
|
|
494
|
+
const paths = _configPathsFor(cwd, ws);
|
|
354
495
|
// #315 — per-call lazy memo: all three detection sites inside this loadConfig
|
|
355
496
|
// call operate on the same cwd and the subrepo set cannot change mid-call, so
|
|
356
497
|
// a single scan is sufficient. The memo is scoped to THIS call (not module-level)
|
|
@@ -364,8 +505,8 @@ function loadConfig(cwd, options = {}) {
|
|
|
364
505
|
return cachedSubRepos.slice();
|
|
365
506
|
};
|
|
366
507
|
let rootParsed = null;
|
|
367
|
-
if (
|
|
368
|
-
const rootConfigPath =
|
|
508
|
+
if (paths.rootConfigPath) {
|
|
509
|
+
const rootConfigPath = paths.rootConfigPath;
|
|
369
510
|
try {
|
|
370
511
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(rootConfigPath);
|
|
371
512
|
if (raw === null)
|
|
@@ -400,7 +541,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
400
541
|
// Root config missing or unparseable — workstream config stands alone
|
|
401
542
|
}
|
|
402
543
|
}
|
|
403
|
-
const configPath =
|
|
544
|
+
const configPath = paths.configPath;
|
|
404
545
|
const defaults = CONFIG_DEFAULTS;
|
|
405
546
|
try {
|
|
406
547
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(configPath);
|
|
@@ -625,7 +766,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
625
766
|
}
|
|
626
767
|
catch {
|
|
627
768
|
// Fall back to ~/.gsd/defaults.json only for truly pre-project contexts (#1683)
|
|
628
|
-
if (
|
|
769
|
+
if (!_readsGlobalDefaults(cwd, ws)) {
|
|
629
770
|
if (rootParsed) {
|
|
630
771
|
// Workstream has no config.json: re-parse using root config as the sole source.
|
|
631
772
|
// (FIX 2: overlay is applied recursively in the re-entrant loadConfig call)
|
|
@@ -642,8 +783,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
642
783
|
}
|
|
643
784
|
}
|
|
644
785
|
try {
|
|
645
|
-
const
|
|
646
|
-
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
786
|
+
const globalDefaultsPath = _globalDefaultsPath();
|
|
647
787
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(globalDefaultsPath);
|
|
648
788
|
if (raw === null)
|
|
649
789
|
throw new Error('missing');
|
|
@@ -698,6 +838,9 @@ function loadConfig(cwd, options = {}) {
|
|
|
698
838
|
}
|
|
699
839
|
module.exports = {
|
|
700
840
|
loadConfig,
|
|
841
|
+
configFilesFor,
|
|
842
|
+
unreadableConfigFiles,
|
|
843
|
+
describeUnreadableConfig,
|
|
701
844
|
isGitIgnored,
|
|
702
845
|
CONFIG_DEFAULTS,
|
|
703
846
|
_getConfigDefault,
|
|
@@ -26,6 +26,21 @@ function isCapabilityConfigKey(keyPath) {
|
|
|
26
26
|
return false;
|
|
27
27
|
return Object.prototype.hasOwnProperty.call(schema, keyPath);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The declared `values` of a capability `type: "enum"` config key, or `null`
|
|
31
|
+
* when the key is not a capability key, is not an enum, or carries no value
|
|
32
|
+
* list. 13-05 / D-07: this is the ONE lookup of enum values —
|
|
33
|
+
* `config-set` asks it rather than restating any list, so the registry stays
|
|
34
|
+
* the single source of what an enum key may hold.
|
|
35
|
+
*/
|
|
36
|
+
function capabilityEnumValues(keyPath) {
|
|
37
|
+
if (!isCapabilityConfigKey(keyPath))
|
|
38
|
+
return null;
|
|
39
|
+
const entry = capabilityRegistry.configSchema[keyPath];
|
|
40
|
+
if (!entry || typeof entry !== 'object')
|
|
41
|
+
return null;
|
|
42
|
+
return entry.type === 'enum' && Array.isArray(entry.values) ? entry.values : null;
|
|
43
|
+
}
|
|
29
44
|
/**
|
|
30
45
|
* Returns true for keys owned by the central schema adapter rather than a
|
|
31
46
|
* federated Capability config slice.
|
|
@@ -53,6 +68,7 @@ module.exports = {
|
|
|
53
68
|
RUNTIME_STATE_KEYS: configuration_cjs_1.RUNTIME_STATE_KEYS,
|
|
54
69
|
DYNAMIC_KEY_PATTERNS: configuration_cjs_1.DYNAMIC_KEY_PATTERNS,
|
|
55
70
|
isCapabilityConfigKey,
|
|
71
|
+
capabilityEnumValues,
|
|
56
72
|
isCentralConfigKey,
|
|
57
73
|
isValidConfigKey,
|
|
58
74
|
};
|
|
@@ -27,10 +27,11 @@ const modelProfiles = require("./model-profiles.cjs");
|
|
|
27
27
|
const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
|
|
28
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
29
|
const configSchema = require("./config-schema.cjs");
|
|
30
|
-
const { VALID_CONFIG_KEYS, isValidConfigKey } = configSchema;
|
|
30
|
+
const { VALID_CONFIG_KEYS, isValidConfigKey, capabilityEnumValues } = configSchema;
|
|
31
31
|
const secrets_cjs_1 = require("./secrets.cjs");
|
|
32
32
|
const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
|
|
33
33
|
const configuration_cjs_1 = require("./configuration.cjs");
|
|
34
|
+
const phase_tier_cjs_1 = require("./phase-tier.cjs");
|
|
34
35
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
35
36
|
const CONFIG_KEY_SUGGESTIONS = {
|
|
36
37
|
'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
|
|
@@ -464,6 +465,53 @@ function setConfigValues(cwd, entries) {
|
|
|
464
465
|
}
|
|
465
466
|
});
|
|
466
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* The ONE membership test every enum guard in `cmdConfigSet` asks (13-REVIEW
|
|
470
|
+
* CR-01). The raw value is parsed before the guards — `true`/`false`/numbers
|
|
471
|
+
* coerced, and a value starting with `[` or `{` JSON-parsed — and
|
|
472
|
+
* `String(['dev']) === 'dev'`, so a membership test on `String(parsedValue)`
|
|
473
|
+
* let a one-element array through and wrote it. Every value list here is
|
|
474
|
+
* strings, so only a plain string can be a member.
|
|
475
|
+
*/
|
|
476
|
+
function isStringMember(list, value) {
|
|
477
|
+
return typeof value === 'string' && list.includes(value);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Config keys `pp-tools config-set` accepts ONLY a real JSON boolean for
|
|
481
|
+
* (07-14 / REV-CR-01).
|
|
482
|
+
*
|
|
483
|
+
* The parse above coerces exactly the two lowercase words `true` and `false`;
|
|
484
|
+
* every other spelling an operator plausibly types becomes a truthy string
|
|
485
|
+
* (`"FALSE"`, `"no"`, `"off"`) or a number. `capability-activation`'s
|
|
486
|
+
* `_resolveActivationValue` ends in `Boolean(r.value)`, so those strings read as
|
|
487
|
+
* ON — an operator typing `config-set vault.enabled FALSE` to switch the vault
|
|
488
|
+
* off instead ARMED every step `capabilities/vault` hangs on that key, against
|
|
489
|
+
* the constraint that a `vault.enabled=false` project stays completely inert.
|
|
490
|
+
*
|
|
491
|
+
* The list, not ten copies of the same `if`: the first seven are capability
|
|
492
|
+
* activation keys read through that `Boolean()` (the vault switches plus every
|
|
493
|
+
* key that gates a blocking hook in the shipped registry). The last three were already guarded individually and are folded in here
|
|
494
|
+
* unchanged, message included, so there is one mechanism in this file rather
|
|
495
|
+
* than two.
|
|
496
|
+
*
|
|
497
|
+
* REFUSE, do not normalise: `FALSE` plainly means false, but `1`, `enabled` and
|
|
498
|
+
* `on` are guesses, and a config-set that silently rewrites the operator's word
|
|
499
|
+
* is the same class of silent-wrong-answer this phase exists to end. A typed
|
|
500
|
+
* usage error names the two accepted values.
|
|
501
|
+
*/
|
|
502
|
+
const BOOLEAN_ONLY_CONFIG_KEYS = Object.freeze([
|
|
503
|
+
'vault.enabled',
|
|
504
|
+
'vault.required',
|
|
505
|
+
'vault.practices_enabled',
|
|
506
|
+
'workflow.schema_drift_gate',
|
|
507
|
+
'workflow.nyquist_validation',
|
|
508
|
+
'workflow.security_enforcement',
|
|
509
|
+
'workflow.ui_safety_gate',
|
|
510
|
+
'pareto.enabled',
|
|
511
|
+
'git.create_tag',
|
|
512
|
+
'workflow.post_planning_gaps',
|
|
513
|
+
'plan_review.source_grounding',
|
|
514
|
+
]);
|
|
467
515
|
/**
|
|
468
516
|
* Command to set a value in the config file, allowing nested values via dot notation (e.g.,
|
|
469
517
|
* "workflow.research").
|
|
@@ -508,12 +556,12 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
508
556
|
catch { /* keep as string */ }
|
|
509
557
|
}
|
|
510
558
|
const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
|
|
511
|
-
if (kp === 'context' && !VALID_CONTEXT_VALUES
|
|
559
|
+
if (kp === 'context' && !isStringMember(VALID_CONTEXT_VALUES, parsedValue)) {
|
|
512
560
|
error(`Invalid context value '${val}'. Valid values: ${VALID_CONTEXT_VALUES.join(', ')}`);
|
|
513
561
|
}
|
|
514
562
|
// Codebase drift detector (#2003)
|
|
515
563
|
const VALID_DRIFT_ACTIONS = ['warn', 'auto-remap'];
|
|
516
|
-
if (kp === 'workflow.drift_action' && !VALID_DRIFT_ACTIONS
|
|
564
|
+
if (kp === 'workflow.drift_action' && !isStringMember(VALID_DRIFT_ACTIONS, parsedValue)) {
|
|
517
565
|
error(`Invalid workflow.drift_action '${val}'. Valid values: ${VALID_DRIFT_ACTIONS.join(', ')}`);
|
|
518
566
|
}
|
|
519
567
|
if (kp === 'workflow.drift_threshold') {
|
|
@@ -521,51 +569,62 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
521
569
|
error(`Invalid workflow.drift_threshold '${val}'. Must be a positive integer.`);
|
|
522
570
|
}
|
|
523
571
|
}
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
// #3086 — git.create_tag: boolean only
|
|
531
|
-
if (kp === 'git.create_tag') {
|
|
532
|
-
if (typeof parsedValue !== 'boolean') {
|
|
533
|
-
error(`Invalid git.create_tag '${val}'. Must be a boolean (true or false).`);
|
|
534
|
-
}
|
|
572
|
+
// Boolean-only keys, in ONE place (07-14 / REV-CR-01; absorbs the individual
|
|
573
|
+
// guards #2493 workflow.post_planning_gaps, #3086 git.create_tag and #22
|
|
574
|
+
// plan_review.source_grounding, whose message this reproduces verbatim).
|
|
575
|
+
if (BOOLEAN_ONLY_CONFIG_KEYS.includes(kp) && typeof parsedValue !== 'boolean') {
|
|
576
|
+
error(`Invalid ${kp} '${val}'. Must be a boolean (true or false).`, ERROR_REASON.USAGE);
|
|
535
577
|
}
|
|
536
578
|
if (kp === 'ship.pr_body_sections') {
|
|
537
579
|
validateShipPrBodySections(parsedValue);
|
|
538
580
|
}
|
|
581
|
+
// Phase ceremony tier (pack 9) — enum, src/phase-tier.cts owns the list
|
|
582
|
+
if (kp === 'phase.tier' && !isStringMember(phase_tier_cjs_1.PHASE_TIERS, parsedValue)) {
|
|
583
|
+
error(`Invalid phase.tier '${val}'. Valid values: ${phase_tier_cjs_1.PHASE_TIERS.join(', ')}`, ERROR_REASON.CONFIG_INVALID_VALUE);
|
|
584
|
+
}
|
|
539
585
|
// Human verification checkpoint mode (#3309)
|
|
540
586
|
const VALID_HUMAN_VERIFY_MODES = ['mid-flight', 'end-of-phase'];
|
|
541
|
-
if (kp === 'workflow.human_verify_mode' && !VALID_HUMAN_VERIFY_MODES
|
|
587
|
+
if (kp === 'workflow.human_verify_mode' && !isStringMember(VALID_HUMAN_VERIFY_MODES, parsedValue)) {
|
|
542
588
|
error(`Invalid workflow.human_verify_mode '${val}'. Valid values: ${VALID_HUMAN_VERIFY_MODES.join(', ')}`);
|
|
543
589
|
}
|
|
544
590
|
// Context position enum validation (#2937)
|
|
545
591
|
const VALID_CONTEXT_POSITIONS = ['front', 'end'];
|
|
546
|
-
if (kp === 'statusline.context_position' && !VALID_CONTEXT_POSITIONS
|
|
592
|
+
if (kp === 'statusline.context_position' && !isStringMember(VALID_CONTEXT_POSITIONS, parsedValue)) {
|
|
547
593
|
error(`Invalid statusline.context_position '${val}'. Valid values: ${VALID_CONTEXT_POSITIONS.join(', ')}`);
|
|
548
594
|
}
|
|
549
595
|
// Fallow scope + profile enum validation (#3424)
|
|
550
596
|
const VALID_FALLOW_SCOPES = ['phase', 'repo'];
|
|
551
|
-
if (kp === 'code_quality.fallow.scope' && !VALID_FALLOW_SCOPES
|
|
597
|
+
if (kp === 'code_quality.fallow.scope' && !isStringMember(VALID_FALLOW_SCOPES, parsedValue)) {
|
|
552
598
|
error(`Invalid code_quality.fallow.scope '${val}'. Valid values: ${VALID_FALLOW_SCOPES.join(', ')}`);
|
|
553
599
|
}
|
|
554
600
|
const VALID_FALLOW_PROFILES = ['minimal', 'standard', 'strict'];
|
|
555
|
-
if (kp === 'code_quality.fallow.profile' && !VALID_FALLOW_PROFILES
|
|
601
|
+
if (kp === 'code_quality.fallow.profile' && !isStringMember(VALID_FALLOW_PROFILES, parsedValue)) {
|
|
556
602
|
error(`Invalid code_quality.fallow.profile '${val}'. Valid values: ${VALID_FALLOW_PROFILES.join(', ')}`);
|
|
557
603
|
}
|
|
558
|
-
// plan_review.source_grounding (#22) — boolean only
|
|
559
|
-
if (kp === 'plan_review.source_grounding') {
|
|
560
|
-
if (typeof parsedValue !== 'boolean') {
|
|
561
|
-
error(`Invalid plan_review.source_grounding '${val}'. Must be a boolean (true or false).`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
604
|
// plan_review.source_grounding_authority (#22) — enum
|
|
565
605
|
const VALID_SOURCE_GROUNDING_AUTHORITIES = ['grep', 'intel', 'treesitter', 'lsp', 'scip'];
|
|
566
|
-
if (kp === 'plan_review.source_grounding_authority' && !VALID_SOURCE_GROUNDING_AUTHORITIES
|
|
606
|
+
if (kp === 'plan_review.source_grounding_authority' && !isStringMember(VALID_SOURCE_GROUNDING_AUTHORITIES, parsedValue)) {
|
|
567
607
|
error(`Invalid plan_review.source_grounding_authority '${val}'. Valid values: ${VALID_SOURCE_GROUNDING_AUTHORITIES.join(', ')}`);
|
|
568
608
|
}
|
|
609
|
+
// Capability enum keys, in ONE place (13-05, D-07). Measured 2026-10-09:
|
|
610
|
+
// `config-set workflow.code_review_depth bogus` wrote at exit 0, as did every
|
|
611
|
+
// other enum key, because no guard consulted the registry's declared `values`. This runs AFTER every
|
|
612
|
+
// hand-coded guard above, so a key that already has its own guard
|
|
613
|
+
// (`workflow.drift_action`) keeps its message byte-identical; the
|
|
614
|
+
// registry, through `capabilityEnumValues`, is the only list consulted
|
|
615
|
+
// here. The refused value is NOT echoed (T-13-03): it is operator text and
|
|
616
|
+
// may carry terminal control sequences. Only the WRITE path changes: how an
|
|
617
|
+
// existing enum key is READ is deliberately unchanged in this phase (D-07).
|
|
618
|
+
// The TYPE is checked before membership (13-REVIEW CR-01): a value starting
|
|
619
|
+
// with `[` is JSON-parsed above, and `String(['standards']) === 'standards'`,
|
|
620
|
+
// so a membership-only test let a one-element array through and wrote it —
|
|
621
|
+
// which `federated-config` silently reads back as the default for an enum
|
|
622
|
+
// key. Every declared capability enum value is a string, so a non-string
|
|
623
|
+
// parse (array, object, number, boolean) can never be a member.
|
|
624
|
+
const enumValues = capabilityEnumValues(kp);
|
|
625
|
+
if (enumValues && !isStringMember(enumValues, parsedValue)) {
|
|
626
|
+
error(`Invalid ${kp}. Valid values: ${enumValues.join(', ')}`, ERROR_REASON.CONFIG_INVALID_VALUE);
|
|
627
|
+
}
|
|
569
628
|
if (kp === 'review.default_reviewers') {
|
|
570
629
|
const normalized = (0, review_reviewer_selection_cjs_1.normalizeConfiguredDefaultReviewers)(parsedValue);
|
|
571
630
|
if (normalized.errors.length > 0) {
|
|
@@ -592,7 +651,58 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
592
651
|
output(maskedResult, raw, `${kp}=${masked}`);
|
|
593
652
|
return;
|
|
594
653
|
}
|
|
595
|
-
|
|
654
|
+
// Pack 6 D-07: the WRITTEN value stays raw; its echo (and the value it
|
|
655
|
+
// replaced) is display-safe.
|
|
656
|
+
const shownValue = redactConfigCoordinates(kp, parsedValue);
|
|
657
|
+
const shownResult = {
|
|
658
|
+
...setConfigValueResult,
|
|
659
|
+
value: redactConfigCoordinates(kp, setConfigValueResult.value),
|
|
660
|
+
previousValue: redactConfigCoordinates(kp, setConfigValueResult.previousValue),
|
|
661
|
+
};
|
|
662
|
+
output(shownResult, raw, `${kp}=${String(shownValue)}`);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Pack 6 D-07: the display-safe form of a config value about to be EMITTED,
|
|
666
|
+
* for the config keys that hold a remote repo coordinate (the shared-tier repo,
|
|
667
|
+
* `SHARED_REPO_CONFIG_KEY`). `keyPath` is the dotted path the value was read
|
|
668
|
+
* at (`''` for the whole config): the key itself, or any ancestor object that
|
|
669
|
+
* carries it, is projected through the one owner (`redactCloneTarget` in
|
|
670
|
+
* src/vault.cts); every other value is returned untouched (and a
|
|
671
|
+
* credential-free coordinate is unchanged by the owner). The file on disk and
|
|
672
|
+
* every in-process reader keep the raw value. Lazily required, so this
|
|
673
|
+
* module's load graph is unchanged.
|
|
674
|
+
*/
|
|
675
|
+
function redactConfigCoordinates(keyPath, value) {
|
|
676
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
677
|
+
const { SHARED_REPO_CONFIG_KEY } = require('./vault-coordinates.cjs');
|
|
678
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
679
|
+
const { redactCloneTarget } = require('./vault.cjs');
|
|
680
|
+
const safe = (v) => (typeof v === 'string' && v !== '' ? (redactCloneTarget(v) ?? v) : v);
|
|
681
|
+
const target = SHARED_REPO_CONFIG_KEY.split('.');
|
|
682
|
+
const at = keyPath === '' ? [] : keyPath.split('.');
|
|
683
|
+
if (at.length > target.length || at.some((seg, i) => seg !== target[i]))
|
|
684
|
+
return value;
|
|
685
|
+
const rest = target.slice(at.length);
|
|
686
|
+
if (rest.length === 0)
|
|
687
|
+
return safe(value);
|
|
688
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
689
|
+
return value;
|
|
690
|
+
const copy = JSON.parse(JSON.stringify(value));
|
|
691
|
+
// The flat form: the whole dotted key written as one top-level property.
|
|
692
|
+
if (at.length === 0 && typeof copy[SHARED_REPO_CONFIG_KEY] === 'string') {
|
|
693
|
+
copy[SHARED_REPO_CONFIG_KEY] = safe(copy[SHARED_REPO_CONFIG_KEY]);
|
|
694
|
+
}
|
|
695
|
+
let node = copy;
|
|
696
|
+
for (const seg of rest.slice(0, -1)) {
|
|
697
|
+
const next = node[seg];
|
|
698
|
+
if (next === null || typeof next !== 'object' || Array.isArray(next))
|
|
699
|
+
return copy;
|
|
700
|
+
node = next;
|
|
701
|
+
}
|
|
702
|
+
const leaf = rest[rest.length - 1];
|
|
703
|
+
if (typeof node[leaf] === 'string')
|
|
704
|
+
node[leaf] = safe(node[leaf]);
|
|
705
|
+
return copy;
|
|
596
706
|
}
|
|
597
707
|
function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
598
708
|
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
@@ -665,7 +775,9 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
665
775
|
output(masked, raw, masked);
|
|
666
776
|
return;
|
|
667
777
|
}
|
|
668
|
-
|
|
778
|
+
// Pack 6 D-07: a remote coordinate (or a section carrying one) is emitted display-safe.
|
|
779
|
+
const shown = redactConfigCoordinates(kp, current);
|
|
780
|
+
output(shown, raw, String(shown));
|
|
669
781
|
}
|
|
670
782
|
/**
|
|
671
783
|
* Command to set the model profile in the config file.
|
|
@@ -768,6 +880,7 @@ function cmdMigrateConfig(cwd, raw) {
|
|
|
768
880
|
}
|
|
769
881
|
module.exports = {
|
|
770
882
|
VALID_CONFIG_KEYS,
|
|
883
|
+
BOOLEAN_ONLY_CONFIG_KEYS,
|
|
771
884
|
cmdConfigEnsureSection,
|
|
772
885
|
cmdConfigSet,
|
|
773
886
|
cmdConfigGet,
|
|
@@ -777,5 +890,7 @@ module.exports = {
|
|
|
777
890
|
cmdMigrateConfig,
|
|
778
891
|
// Exported for programmatic use by capability-writer and tests
|
|
779
892
|
setConfigValue,
|
|
893
|
+
// Pack 6 D-07: `state load` emits the whole config through the same projection.
|
|
894
|
+
redactConfigCoordinates,
|
|
780
895
|
setConfigValues,
|
|
781
896
|
};
|
|
@@ -61,22 +61,58 @@ function detectSubRepos(cwd) {
|
|
|
61
61
|
// ─── Summary body helpers ─────────────────────────────────────────────────
|
|
62
62
|
/**
|
|
63
63
|
* Extract a one-liner from the summary body when it's not in frontmatter.
|
|
64
|
+
*
|
|
65
|
+
* The slot is the one the SUMMARY template defines (`template.cts` summary body,
|
|
66
|
+
* `templates/summary.md:51` and its `<one_liner_rules>`): the FIRST paragraph
|
|
67
|
+
* directly after the FIRST `# ` title, written either as a whole bold paragraph
|
|
68
|
+
* (a wrapped one is joined with single spaces, the outer `**` stripped once and
|
|
69
|
+
* inner bold spans kept) or as the `**Label:** prose` line bug #2660 pinned.
|
|
70
|
+
* Anything else — a plain-prose first paragraph, or a bold span under a later
|
|
71
|
+
* heading — yields null. 261007-rly D-3/D-8: the old pattern took the first bold
|
|
72
|
+
* span after ANY heading anywhere in the file, which put fragments such as
|
|
73
|
+
* `Task 1` and `D-08-32 — applied as written.` into 15 of the 34 accomplishment
|
|
74
|
+
* lines of the v1.1 close (todos/done/milestone-complete-writes-a-poor-milestone-record.md).
|
|
75
|
+
* Shared by `milestone complete` and `summary-extract`'s `one_liner`.
|
|
64
76
|
*/
|
|
65
77
|
function extractOneLinerFromBody(content) {
|
|
66
78
|
if (!content)
|
|
67
79
|
return null;
|
|
68
80
|
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
69
81
|
const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
|
70
|
-
const
|
|
71
|
-
|
|
82
|
+
const lines = body.split('\n');
|
|
83
|
+
const title = lines.findIndex((l) => /^# /.test(l));
|
|
84
|
+
if (title === -1)
|
|
85
|
+
return null;
|
|
86
|
+
let i = title + 1;
|
|
87
|
+
while (i < lines.length && lines[i].trim() === '')
|
|
88
|
+
i++;
|
|
89
|
+
const paragraph = [];
|
|
90
|
+
while (i < lines.length && lines[i].trim() !== '' && !/^#{1,6}\s/.test(lines[i]))
|
|
91
|
+
paragraph.push(lines[i++]);
|
|
92
|
+
if (paragraph.length === 0)
|
|
72
93
|
return null;
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
if (/:\s*$/.test(
|
|
76
|
-
const prose =
|
|
94
|
+
// `**Label:** prose` (bug #2660): the prose after the label, null when empty.
|
|
95
|
+
const label = paragraph[0].match(/^\*\*([^*\n]+)\*\*([^\n]*)/);
|
|
96
|
+
if (label && /:\s*$/.test(label[1].trim())) {
|
|
97
|
+
const prose = label[2].trim();
|
|
77
98
|
return prose.length > 0 ? prose : null;
|
|
78
99
|
}
|
|
79
|
-
|
|
100
|
+
// A whole bold paragraph, single-line or wrapped.
|
|
101
|
+
const first = paragraph[0].trim();
|
|
102
|
+
const last = paragraph[paragraph.length - 1].trim();
|
|
103
|
+
if (!first.startsWith('**'))
|
|
104
|
+
return null;
|
|
105
|
+
let bold = null;
|
|
106
|
+
if (last.endsWith('**'))
|
|
107
|
+
bold = paragraph.map((l) => l.trim()).join(' ');
|
|
108
|
+
// A bold first line with prose glued under it (no blank line between) — the
|
|
109
|
+
// shape tests/core-utils.test.cjs pins; the bold line is still the slot.
|
|
110
|
+
else if (first.length > 4 && first.endsWith('**'))
|
|
111
|
+
bold = first;
|
|
112
|
+
if (bold === null)
|
|
113
|
+
return null;
|
|
114
|
+
const inner = bold.slice(2, -2).trim();
|
|
115
|
+
return inner.length > 0 ? inner : null;
|
|
80
116
|
}
|
|
81
117
|
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
|
82
118
|
function pathExistsInternal(cwd, targetPath) {
|