create-principles-disciple 1.147.7 → 1.147.9
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/_release/product-identity.json +2 -2
- package/core/dist/host/host-installer.d.ts +15 -0
- package/core/dist/host/host-installer.d.ts.map +1 -1
- package/core/dist/host/host-installer.js.map +1 -1
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.d.ts +37 -0
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.js +126 -0
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.js.map +1 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.d.ts +46 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.js +91 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.js.map +1 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +2 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +2 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +2 -2
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +3 -0
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +2 -0
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-recall-capability.js +53 -2
- package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -1
- package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-recall-read-model.js +60 -5
- package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js +35 -0
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
- package/core/dist/runtime-v2/types/event-types.d.ts +6 -3
- package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/event-types.js +2 -0
- package/core/dist/runtime-v2/types/event-types.js.map +1 -1
- package/core/package.json +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -29
- package/dist/index.js.map +1 -1
- package/dist/install-summary.d.ts +41 -0
- package/dist/install-summary.d.ts.map +1 -0
- package/dist/install-summary.js +123 -0
- package/dist/install-summary.js.map +1 -0
- package/dist/installer.d.ts +36 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +120 -26
- package/dist/installer.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts +70 -17
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +571 -186
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/prompts.js.map +1 -1
- package/host-runtime/dist/active-principle-prompt.d.ts +28 -0
- package/host-runtime/dist/active-principle-prompt.js +163 -50
- package/host-runtime/dist/adaptive-principle-revalidation.d.ts +8 -0
- package/host-runtime/dist/adaptive-principle-revalidation.js +52 -0
- package/host-runtime/dist/governance-signal-admission.d.ts +2 -2
- package/host-runtime/dist/governance-signal-admission.js +2 -6
- package/host-runtime/dist/index.d.ts +16 -0
- package/host-runtime/dist/index.js +34 -0
- package/host-runtime/dist/prompt-activation-authority.d.ts +7 -0
- package/host-runtime/dist/prompt-activation-authority.js +16 -0
- package/host-runtime/package.json +1 -1
- package/package.json +3 -3
- package/pd-cli/dist/commands/health-mcp.d.ts +19 -0
- package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -1
- package/pd-cli/dist/commands/health-mcp.js +226 -0
- package/pd-cli/dist/commands/health-mcp.js.map +1 -1
- package/pd-cli/dist/commands/health.d.ts.map +1 -1
- package/pd-cli/dist/commands/health.js +18 -1
- package/pd-cli/dist/commands/health.js.map +1 -1
- package/pd-cli/dist/commands/mcp-logs-export.d.ts +7 -0
- package/pd-cli/dist/commands/mcp-logs-export.d.ts.map +1 -0
- package/pd-cli/dist/commands/mcp-logs-export.js +267 -0
- package/pd-cli/dist/commands/mcp-logs-export.js.map +1 -0
- package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -1
- package/pd-cli/dist/commands/workbuddy-worker.js +2 -1
- package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -1
- package/pd-cli/dist/index.js +35 -0
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/dist/services/zip-writer.d.ts +6 -0
- package/pd-cli/dist/services/zip-writer.d.ts.map +1 -0
- package/pd-cli/dist/services/zip-writer.js +97 -0
- package/pd-cli/dist/services/zip-writer.js.map +1 -0
- package/pd-cli/package.json +1 -1
- package/pd-mcp-server/dist/index.d.ts +1 -0
- package/pd-mcp-server/dist/index.js +167 -10
- package/pd-mcp-server/dist/log-file.d.ts +26 -0
- package/pd-mcp-server/dist/log-file.js +312 -0
- package/pd-mcp-server/dist/logging.d.ts +2 -1
- package/pd-mcp-server/dist/logging.js +25 -14
- package/pd-mcp-server/dist/server.js +78 -24
- package/pd-mcp-server/dist/version.d.ts +1 -1
- package/pd-mcp-server/dist/version.js +1 -1
- package/pd-mcp-server/dist/workspace.d.ts +3 -0
- package/pd-mcp-server/dist/workspace.js +5 -0
- package/pd-mcp-server/package.json +1 -1
- package/plugin/dist/bundle.js +446 -445
- package/plugin/dist/core/event-log.d.ts +3 -0
- package/plugin/dist/governance-audit.js +3 -3
- package/plugin/dist/hooks/adaptive-activation-config.d.ts +8 -0
- package/plugin/dist/hooks/prompt-helpers.d.ts +5 -0
- package/plugin/dist/hooks/prompt-types.d.ts +1 -0
- package/plugin/dist/hooks/prompt.d.ts +8 -1
- package/plugin/dist/host-runtime/openclaw-host-runtime.d.ts +13 -0
- package/plugin/dist/openclaw-sdk.d.ts +4 -0
- package/plugin/dist/openclaw.plugin.json +33 -1
- package/plugin/mcp/launcher.cjs +561 -0
- package/plugin/openclaw.plugin.json +33 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/installer.js +120 -26
- package/release-manager/dist/installers/workbuddy-host-installer.js +571 -186
- package/release-manager/package.json +1 -1
- package/workbuddy-adapter/dist/pd-hook.js +3 -2
- package/workbuddy-adapter/package.json +1 -1
- package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +1 -1
- package/workbuddy-adapter/plugin/mcp/launcher.cjs +215 -1
- package/workbuddy-adapter/plugin/skills/pd-pain-signal/SKILL.md +4 -0
|
@@ -418,7 +418,7 @@ export function findWorkbuddyPluginPayload(pluginDir, runtimeAdapterDir) {
|
|
|
418
418
|
return { reason: `workbuddy_plugin_control_missing: ${root} must ship scripts/pd-control.cjs for the pd-disable skill.` };
|
|
419
419
|
}
|
|
420
420
|
const { version } = manifest;
|
|
421
|
-
return { payload: { pluginDir: root, version } };
|
|
421
|
+
return { payload: { pluginDir: root, version, defaultEnabled: manifest.defaultEnabled === true } };
|
|
422
422
|
}
|
|
423
423
|
return {
|
|
424
424
|
reason: `workbuddy_adapter_payload_missing: no WorkBuddy plugin skeleton found (looked for workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json under the installer payload and ${runtimeAdapterDir}). The @principles/workbuddy-adapter component is not part of this installer build.`,
|
|
@@ -515,10 +515,7 @@ function removeWorkbuddyFromInstallManifestHosts(manifestPath) {
|
|
|
515
515
|
}
|
|
516
516
|
// ─── Feature flag override (config.yaml line-targeted edit) ─────────────────
|
|
517
517
|
const FEATURES_LINE = /^features:\s*(?:#.*)?$/;
|
|
518
|
-
const FEATURES_EMPTY_MAPPING_LINE = /^features:\s*\{\}\s*(?:#.*)?$/;
|
|
519
|
-
const WORKBUDDY_KEY_LINE = /^ {2}host\.workbuddy:\s*(?:#.*)?$/;
|
|
520
|
-
const WORKBUDDY_ENABLED_LINE = /^ {4}enabled:\s*(?:true|false)\s*(?:#.*)?$/;
|
|
521
|
-
const WORKBUDDY_CATEGORY_LINE = /^ {4}category:(?:\s.*)?(?:#.*)?$/;
|
|
518
|
+
const FEATURES_EMPTY_MAPPING_LINE = /^features:\s*\{\s*\}\s*(?:#.*)?$/;
|
|
522
519
|
/**
|
|
523
520
|
* Registry-required category for the host.workbuddy flag entry. SSoT:
|
|
524
521
|
* packages/principles-core/src/runtime-v2/feature-flags/feature-flag-contract.ts
|
|
@@ -533,8 +530,318 @@ function stripYamlComment(line) {
|
|
|
533
530
|
const hash = line.indexOf('#');
|
|
534
531
|
return (hash === -1 ? line : line.slice(0, hash)).trim();
|
|
535
532
|
}
|
|
536
|
-
function
|
|
537
|
-
return
|
|
533
|
+
function leadingSpaces(line) {
|
|
534
|
+
return /^ +/.exec(line)?.[0].length ?? 0;
|
|
535
|
+
}
|
|
536
|
+
function escapeRegExp(value) {
|
|
537
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
538
|
+
}
|
|
539
|
+
const FLAG_EDIT_MANUAL_GUIDANCE = 'Edit the governance config.yaml by hand: keep the block-style mapping and change only features.<key>.enabled (or add the key under the top-level features: mapping). The installer refuses whole-file rewrites so Owner comments and unknown fields survive.';
|
|
540
|
+
function failLineEdit(configPath, code, detail) {
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
reason: `${code}: ${detail} (${configPath}). No write was performed.`,
|
|
544
|
+
nextAction: `${FLAG_EDIT_MANUAL_GUIDANCE} Affected file: ${configPath}.`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function inspectFeaturesHeader(lines) {
|
|
548
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
549
|
+
const line = lines[index];
|
|
550
|
+
if (typeof line !== 'string' || !/^features:/.test(line))
|
|
551
|
+
continue;
|
|
552
|
+
if (FEATURES_LINE.test(line))
|
|
553
|
+
return { index, style: 'block' };
|
|
554
|
+
if (FEATURES_EMPTY_MAPPING_LINE.test(line))
|
|
555
|
+
return { index, style: 'empty' };
|
|
556
|
+
return { index, style: 'flow' };
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
function findUnsafeYamlConstruct(lines, start) {
|
|
561
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
562
|
+
const line = lines[index];
|
|
563
|
+
if (typeof line !== 'string')
|
|
564
|
+
break;
|
|
565
|
+
const body = stripYamlComment(line);
|
|
566
|
+
if (index > start && body.trim() === '')
|
|
567
|
+
continue;
|
|
568
|
+
if (index > start && !/^\s/.test(line))
|
|
569
|
+
break;
|
|
570
|
+
if (/^\s*<<:/.test(body))
|
|
571
|
+
return { line: index, kind: 'YAML merge key (<<:)' };
|
|
572
|
+
if (/(?:^|\s)&[A-Za-z0-9_]/.test(body))
|
|
573
|
+
return { line: index, kind: 'YAML anchor (&)' };
|
|
574
|
+
if (/(?:^|\s)\*[A-Za-z0-9_]/.test(body))
|
|
575
|
+
return { line: index, kind: 'YAML alias (*)' };
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
function featuresBlockEnd(lines, start) {
|
|
580
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
581
|
+
const line = lines[index];
|
|
582
|
+
if (typeof line !== 'string')
|
|
583
|
+
return index;
|
|
584
|
+
const body = line.trim();
|
|
585
|
+
if (body === '' || body.startsWith('#'))
|
|
586
|
+
continue;
|
|
587
|
+
if (!/^\s/.test(line))
|
|
588
|
+
return index;
|
|
589
|
+
}
|
|
590
|
+
return lines.length;
|
|
591
|
+
}
|
|
592
|
+
function findKeyLineIndexes(lines, range, flagId) {
|
|
593
|
+
const indexes = [];
|
|
594
|
+
for (let index = range.start + 1; index < range.end; index += 1) {
|
|
595
|
+
const line = lines[index];
|
|
596
|
+
if (typeof line !== 'string')
|
|
597
|
+
break;
|
|
598
|
+
if (stripYamlComment(line).trim() === `${flagId}:`)
|
|
599
|
+
indexes.push(index);
|
|
600
|
+
}
|
|
601
|
+
return indexes;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Detect a flag entry written inline (`flagId: { enabled: true }` or
|
|
605
|
+
* `flagId: [ ... ]`) inside the features block. Such an entry has no
|
|
606
|
+
* independent `enabled:` line to rewrite, so it is refused explicitly rather
|
|
607
|
+
* than reported as an unparsable rewrite after a duplicate key is inserted.
|
|
608
|
+
*/
|
|
609
|
+
function findInlineFlagEntry(lines, range, flagId) {
|
|
610
|
+
const pattern = new RegExp(`^${escapeRegExp(flagId)}:\\s*[\\[{]`);
|
|
611
|
+
for (let index = range.start + 1; index < range.end; index += 1) {
|
|
612
|
+
const line = lines[index];
|
|
613
|
+
if (typeof line !== 'string')
|
|
614
|
+
break;
|
|
615
|
+
if (pattern.test(stripYamlComment(line).trim()))
|
|
616
|
+
return index;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
function flagBlockEnd(lines, keyIndex, keyIndent) {
|
|
621
|
+
for (let index = keyIndex + 1; index < lines.length; index += 1) {
|
|
622
|
+
const line = lines[index];
|
|
623
|
+
if (typeof line !== 'string')
|
|
624
|
+
return index;
|
|
625
|
+
const body = line.trim();
|
|
626
|
+
if (body === '' || body.startsWith('#'))
|
|
627
|
+
continue;
|
|
628
|
+
if (leadingSpaces(line) <= keyIndent)
|
|
629
|
+
return index;
|
|
630
|
+
}
|
|
631
|
+
return lines.length;
|
|
632
|
+
}
|
|
633
|
+
function childIndentOf(lines, keyIndex, options) {
|
|
634
|
+
for (let index = keyIndex + 1; index < options.end; index += 1) {
|
|
635
|
+
const line = lines[index];
|
|
636
|
+
if (typeof line !== 'string')
|
|
637
|
+
continue;
|
|
638
|
+
const body = line.trim();
|
|
639
|
+
if (body === '' || body.startsWith('#'))
|
|
640
|
+
continue;
|
|
641
|
+
const indent = leadingSpaces(line);
|
|
642
|
+
if (indent > options.keyIndent)
|
|
643
|
+
return indent;
|
|
644
|
+
}
|
|
645
|
+
return options.keyIndent + 2;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Infer the child indentation used by an existing features block, so a
|
|
649
|
+
* missing flag is inserted in the file's own style (2, 4, 6 spaces …) instead
|
|
650
|
+
* of a guessed `key + 2`. Returns null when the block has no nested content to
|
|
651
|
+
* learn from.
|
|
652
|
+
*/
|
|
653
|
+
function inferChildIndent(lines, range, keyIndent) {
|
|
654
|
+
let parentIndent = null;
|
|
655
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
656
|
+
const line = lines[index];
|
|
657
|
+
if (typeof line !== 'string')
|
|
658
|
+
continue;
|
|
659
|
+
const body = line.trim();
|
|
660
|
+
if (body === '' || body.startsWith('#'))
|
|
661
|
+
continue;
|
|
662
|
+
const indent = leadingSpaces(line);
|
|
663
|
+
if (parentIndent !== null && indent > parentIndent)
|
|
664
|
+
return indent;
|
|
665
|
+
parentIndent = indent === keyIndent ? indent : null;
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
function findFieldIndexes(lines, range, options) {
|
|
670
|
+
const pattern = new RegExp(`^ {${options.indent}}${escapeRegExp(options.field)}:(\\s|$)`);
|
|
671
|
+
const indexes = [];
|
|
672
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
673
|
+
const line = lines[index];
|
|
674
|
+
if (typeof line !== 'string')
|
|
675
|
+
continue;
|
|
676
|
+
if (pattern.test(line))
|
|
677
|
+
indexes.push(index);
|
|
678
|
+
}
|
|
679
|
+
return indexes;
|
|
680
|
+
}
|
|
681
|
+
function rewriteScalarLine(line, field, value) {
|
|
682
|
+
const match = new RegExp(`^(\\s*)${escapeRegExp(field)}:(\\s*)(.*)$`).exec(line);
|
|
683
|
+
if (!match)
|
|
684
|
+
return null;
|
|
685
|
+
const remainder = match[3] ?? '';
|
|
686
|
+
const hash = remainder.indexOf('#');
|
|
687
|
+
const comment = hash === -1 ? '' : remainder.slice(hash);
|
|
688
|
+
const valuePart = hash === -1 ? remainder : remainder.slice(0, hash);
|
|
689
|
+
const trailing = /\s*$/.exec(valuePart)?.[0] ?? '';
|
|
690
|
+
const rawValue = valuePart.trim();
|
|
691
|
+
if (rawValue === '')
|
|
692
|
+
return null;
|
|
693
|
+
if (/^(?:~|null|Null|NULL)$/.test(rawValue))
|
|
694
|
+
return null;
|
|
695
|
+
const quoted = /^(['"])([^'"]*)(['"])$/.exec(rawValue);
|
|
696
|
+
const quote = quoted ? quoted[1] : '';
|
|
697
|
+
return `${match[1]}${field}:${match[2]}${quote}${value}${quote}${trailing}${comment}`;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Locate `features.<flagId>` in block-style YAML and rewrite ONLY its
|
|
701
|
+
* `enabled` line (plus, when missing, its registry `category` line). Every
|
|
702
|
+
* byte outside the flag block is verified unchanged before writing; anything
|
|
703
|
+
* the editor cannot locate safely fails closed instead of falling back to a
|
|
704
|
+
* whole-file `yaml.dump` (which would drop Owner comments).
|
|
705
|
+
*
|
|
706
|
+
* KNOWN LIMITATIONS (all fail closed, never rewritten; the first two are
|
|
707
|
+
* rejected by the YAML parse that runs before any edit):
|
|
708
|
+
* - tab-based indentation (invalid YAML);
|
|
709
|
+
* - any config that does not parse;
|
|
710
|
+
* - `features:` or the flag entry written in flow style (`{ ... }` / `[ ... ]`);
|
|
711
|
+
* - YAML anchors, aliases or merge keys (`&`, `*`, `<<:`) inside features;
|
|
712
|
+
* - a flag appearing twice, or the same field twice inside one flag block;
|
|
713
|
+
* - a field that is not a scalar (`enabled`/`category` written as a nested map).
|
|
714
|
+
*/
|
|
715
|
+
function applyFeatureFlagLineEdit(raw, request) {
|
|
716
|
+
const { flagId, enabled, category, configPath } = request;
|
|
717
|
+
// Preserve the file's own line ending: inserting "\n" into a CRLF config
|
|
718
|
+
// would change every following byte's neighbourhood (mixed EOL).
|
|
719
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
720
|
+
const lines = raw.split(eol);
|
|
721
|
+
const header = inspectFeaturesHeader(lines);
|
|
722
|
+
if (header?.style === 'flow') {
|
|
723
|
+
return failLineEdit(configPath, 'config_features_flow_style', 'the top-level features: mapping is written inline (flow style)');
|
|
724
|
+
}
|
|
725
|
+
if (header) {
|
|
726
|
+
const unsafe = findUnsafeYamlConstruct(lines, header.index);
|
|
727
|
+
if (unsafe !== null) {
|
|
728
|
+
return failLineEdit(configPath, 'config_yaml_anchor_or_merge_key', `${unsafe.kind} at line ${unsafe.line + 1}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const featuresStart = header?.index ?? lines.length;
|
|
732
|
+
const featuresEnd = header ? featuresBlockEnd(lines, header.index) : lines.length;
|
|
733
|
+
const featuresRange = { start: featuresStart, end: featuresEnd };
|
|
734
|
+
if (header) {
|
|
735
|
+
const inline = findInlineFlagEntry(lines, featuresRange, flagId);
|
|
736
|
+
if (inline !== null) {
|
|
737
|
+
return failLineEdit(configPath, 'config_flag_flow_style', `features.${flagId} is written inline at line ${inline + 1}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const keyIndexes = header ? findKeyLineIndexes(lines, featuresRange, flagId) : [];
|
|
741
|
+
if (keyIndexes.length > 1) {
|
|
742
|
+
return failLineEdit(configPath, 'config_flag_block_ambiguous', `features.${flagId} appears ${keyIndexes.length} times`);
|
|
743
|
+
}
|
|
744
|
+
let working;
|
|
745
|
+
let allowedStart;
|
|
746
|
+
let allowedEnd;
|
|
747
|
+
if (keyIndexes.length === 1) {
|
|
748
|
+
const [keyIndex] = keyIndexes;
|
|
749
|
+
const keyLine = keyIndex === undefined ? undefined : lines[keyIndex];
|
|
750
|
+
if (keyIndex === undefined || keyLine === undefined) {
|
|
751
|
+
return failLineEdit(configPath, 'config_flag_block_ambiguous', `features.${flagId} could not be located`);
|
|
752
|
+
}
|
|
753
|
+
const keyIndent = leadingSpaces(keyLine);
|
|
754
|
+
const blockEnd = flagBlockEnd(lines, keyIndex, keyIndent);
|
|
755
|
+
const childIndent = childIndentOf(lines, keyIndex, { end: blockEnd, keyIndent });
|
|
756
|
+
working = [...lines];
|
|
757
|
+
const enabledIndexes = findFieldIndexes(working, { start: keyIndex + 1, end: blockEnd }, { field: 'enabled', indent: childIndent });
|
|
758
|
+
if (enabledIndexes.length > 1) {
|
|
759
|
+
return failLineEdit(configPath, 'config_flag_field_duplicate', `features.${flagId}.enabled appears ${enabledIndexes.length} times`);
|
|
760
|
+
}
|
|
761
|
+
const [existingEnabled] = enabledIndexes;
|
|
762
|
+
let enabledIndex;
|
|
763
|
+
if (existingEnabled !== undefined) {
|
|
764
|
+
const enabledLine = working[existingEnabled];
|
|
765
|
+
const rewritten = typeof enabledLine === 'string' ? rewriteScalarLine(enabledLine, 'enabled', String(enabled)) : null;
|
|
766
|
+
if (rewritten === null) {
|
|
767
|
+
return failLineEdit(configPath, 'config_flag_field_unsupported', `features.${flagId}.enabled is not a scalar boolean`);
|
|
768
|
+
}
|
|
769
|
+
working[existingEnabled] = rewritten;
|
|
770
|
+
enabledIndex = existingEnabled;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
enabledIndex = keyIndex + 1;
|
|
774
|
+
working.splice(enabledIndex, 0, `${' '.repeat(childIndent)}enabled: ${enabled}`);
|
|
775
|
+
}
|
|
776
|
+
const blockEndAfterInsert = flagBlockEnd(working, keyIndex, keyIndent);
|
|
777
|
+
const categoryIndexes = findFieldIndexes(working, { start: keyIndex + 1, end: blockEndAfterInsert }, { field: 'category', indent: childIndent });
|
|
778
|
+
if (categoryIndexes.length > 1) {
|
|
779
|
+
return failLineEdit(configPath, 'config_flag_field_duplicate', `features.${flagId}.category appears ${categoryIndexes.length} times`);
|
|
780
|
+
}
|
|
781
|
+
const [existingCategory] = categoryIndexes;
|
|
782
|
+
if (existingCategory !== undefined) {
|
|
783
|
+
const categoryLine = working[existingCategory];
|
|
784
|
+
const rewritten = typeof categoryLine === 'string' ? rewriteScalarLine(categoryLine, 'category', category) : null;
|
|
785
|
+
if (rewritten === null) {
|
|
786
|
+
return failLineEdit(configPath, 'config_flag_field_unsupported', `features.${flagId}.category is not a scalar string`);
|
|
787
|
+
}
|
|
788
|
+
working[existingCategory] = rewritten;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
working.splice(enabledIndex + 1, 0, `${' '.repeat(childIndent)}category: ${category}`);
|
|
792
|
+
}
|
|
793
|
+
allowedStart = keyIndex;
|
|
794
|
+
allowedEnd = blockEnd;
|
|
795
|
+
}
|
|
796
|
+
else if (header === null) {
|
|
797
|
+
working = [
|
|
798
|
+
...lines,
|
|
799
|
+
'features:',
|
|
800
|
+
` ${flagId}:`,
|
|
801
|
+
` enabled: ${enabled}`,
|
|
802
|
+
` category: ${category}`,
|
|
803
|
+
];
|
|
804
|
+
allowedStart = lines.length;
|
|
805
|
+
allowedEnd = lines.length;
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
working = [...lines];
|
|
809
|
+
let keyIndent = 2;
|
|
810
|
+
const firstChild = working.slice(header.index + 1, featuresEnd).find((line) => {
|
|
811
|
+
if (typeof line !== 'string')
|
|
812
|
+
return false;
|
|
813
|
+
const body = line.trim();
|
|
814
|
+
return body !== '' && !body.startsWith('#');
|
|
815
|
+
});
|
|
816
|
+
if (typeof firstChild === 'string')
|
|
817
|
+
keyIndent = leadingSpaces(firstChild);
|
|
818
|
+
const childIndent = inferChildIndent(working, { start: header.index + 1, end: featuresEnd }, keyIndent) ?? keyIndent + 2;
|
|
819
|
+
const insertAt = header.index + 1;
|
|
820
|
+
if (header.style === 'empty')
|
|
821
|
+
working[header.index] = 'features:';
|
|
822
|
+
working.splice(insertAt, 0, `${' '.repeat(keyIndent)}${flagId}:`, `${' '.repeat(childIndent)}enabled: ${enabled}`, `${' '.repeat(childIndent)}category: ${category}`);
|
|
823
|
+
allowedStart = header.style === 'empty' ? header.index : insertAt;
|
|
824
|
+
allowedEnd = header.style === 'empty' ? header.index + 1 : insertAt;
|
|
825
|
+
}
|
|
826
|
+
const next = working.join(eol);
|
|
827
|
+
const prefix = lines.slice(0, allowedStart).join(eol);
|
|
828
|
+
const suffix = lines.slice(allowedEnd).join(eol);
|
|
829
|
+
if (!next.startsWith(prefix) || !next.endsWith(suffix)) {
|
|
830
|
+
return failLineEdit(configPath, 'config_edit_out_of_scope', `bytes outside features.${flagId} would change`);
|
|
831
|
+
}
|
|
832
|
+
let parsed;
|
|
833
|
+
try {
|
|
834
|
+
parsed = yaml.load(next);
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return failLineEdit(configPath, 'config_rewrite_invalid', `the edited YAML no longer parses`);
|
|
838
|
+
}
|
|
839
|
+
const features = isRecord(parsed) ? parsed.features : undefined;
|
|
840
|
+
const entry = isRecord(features) ? features[flagId] : undefined;
|
|
841
|
+
if (!isRecord(entry) || entry.enabled !== enabled || entry.category !== category) {
|
|
842
|
+
return failLineEdit(configPath, 'config_rewrite_verification_failed', `features.${flagId} did not round-trip to enabled=${String(enabled)}, category=${category}`);
|
|
843
|
+
}
|
|
844
|
+
return { ok: true, next };
|
|
538
845
|
}
|
|
539
846
|
/**
|
|
540
847
|
* Enable/disable `features.host.workbuddy.enabled` in the GOVERNANCE
|
|
@@ -551,75 +858,198 @@ function isEmptyFeaturesMapping(line) {
|
|
|
551
858
|
* skipped with host.workbuddy_disabled.
|
|
552
859
|
* Both entries are validated and atomically committed in one file write;
|
|
553
860
|
* an already-correct config is left byte-identical (no rewrite).
|
|
861
|
+
*
|
|
862
|
+
* 2026-11-05 (Owner decision): `workbuddy_live_keyword_pain` is written
|
|
863
|
+
* `enabled: false` unconditionally. WorkBuddy has no persisted observation
|
|
864
|
+
* writer, so the automatic keyword path can only end as `observation_only`
|
|
865
|
+
* (an abandoned marker, never a Pain row). Leaving the flag ON made install
|
|
866
|
+
* output claim an automatic capability that does not exist. The flag id stays
|
|
867
|
+
* (it is part of the feature-flag contract and may exist in old configs), but
|
|
868
|
+
* the installer never turns it on.
|
|
554
869
|
*/
|
|
555
|
-
export function ensureWorkbuddyGovernanceEnabled(governanceDir) {
|
|
870
|
+
export function ensureWorkbuddyGovernanceEnabled(governanceDir, options = {}) {
|
|
871
|
+
const persist = options.persist !== false;
|
|
556
872
|
const configPath = path.join(governanceDir, '.pd', 'config.yaml');
|
|
873
|
+
let rawText;
|
|
874
|
+
try {
|
|
875
|
+
rawText = readFileSync(configPath, 'utf-8');
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return {
|
|
879
|
+
ok: false,
|
|
880
|
+
reason: 'fresh_workbuddy_config_unreadable',
|
|
881
|
+
nextAction: `Fix the YAML by hand (the installer never rewrites the whole file), then retry. Affected file: ${configPath}.`,
|
|
882
|
+
hostExplicitOff: false,
|
|
883
|
+
keywordExplicitOff: false,
|
|
884
|
+
keywordPainEnabled: false,
|
|
885
|
+
changed: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
557
888
|
let raw;
|
|
558
889
|
try {
|
|
559
|
-
raw = yaml.load(
|
|
890
|
+
raw = yaml.load(rawText);
|
|
560
891
|
}
|
|
561
892
|
catch {
|
|
562
|
-
return {
|
|
893
|
+
return {
|
|
894
|
+
ok: false,
|
|
895
|
+
reason: 'fresh_workbuddy_config_unreadable',
|
|
896
|
+
nextAction: `Fix the YAML by hand (the installer never rewrites the whole file), then retry. Affected file: ${configPath}.`,
|
|
897
|
+
hostExplicitOff: false,
|
|
898
|
+
keywordExplicitOff: false,
|
|
899
|
+
keywordPainEnabled: false,
|
|
900
|
+
changed: false,
|
|
901
|
+
};
|
|
563
902
|
}
|
|
564
903
|
if (!isRecord(raw) || !isRecord(raw.features)) {
|
|
565
|
-
return { ok: false, reason: 'fresh_workbuddy_features_invalid', nextAction: 'Governance config needs a features mapping.', hostExplicitOff: false, keywordExplicitOff: false, changed: false };
|
|
904
|
+
return { ok: false, reason: 'fresh_workbuddy_features_invalid', nextAction: 'Governance config needs a features mapping.', hostExplicitOff: false, keywordExplicitOff: false, keywordPainEnabled: false, changed: false };
|
|
566
905
|
}
|
|
567
906
|
const oldHost = raw.features['host.workbuddy'];
|
|
568
907
|
const oldKeyword = raw.features.workbuddy_live_keyword_pain;
|
|
569
908
|
const hostExplicitOff = isRecord(oldHost) && oldHost.enabled === false;
|
|
570
909
|
const keywordExplicitOff = isRecord(oldKeyword) && oldKeyword.enabled === false;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
910
|
+
// Read-only mode (`status`): report the gates the config ACTUALLY holds and
|
|
911
|
+
// never write. A flag that is absent is OFF (the registry default), never the
|
|
912
|
+
// install-time projection — guessing ON would misreport a config that does
|
|
913
|
+
// not set the gate. The install path keeps its migration below.
|
|
914
|
+
if (!persist) {
|
|
915
|
+
return {
|
|
916
|
+
ok: true, configPath,
|
|
917
|
+
enabled: isRecord(oldHost) && oldHost.enabled === true,
|
|
918
|
+
hostExplicitOff, keywordExplicitOff,
|
|
919
|
+
keywordPainEnabled: isRecord(oldKeyword) && oldKeyword.enabled === true, changed: false,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
577
922
|
const alreadyCorrect = isRecord(oldHost) && oldHost.enabled === !hostExplicitOff && oldHost.category === WORKBUDDY_FLAG_CATEGORY
|
|
578
|
-
&& isRecord(oldKeyword) && oldKeyword.enabled ===
|
|
923
|
+
&& isRecord(oldKeyword) && oldKeyword.enabled === false && oldKeyword.category === WORKBUDDY_FLAG_CATEGORY;
|
|
579
924
|
if (alreadyCorrect) {
|
|
580
|
-
return { ok: true, configPath, enabled: !hostExplicitOff, hostExplicitOff, keywordExplicitOff, changed: false };
|
|
925
|
+
return { ok: true, configPath, enabled: !hostExplicitOff, hostExplicitOff, keywordExplicitOff, keywordPainEnabled: false, changed: false };
|
|
581
926
|
}
|
|
927
|
+
// Line-targeted edits: only the two flag blocks change; every comment,
|
|
928
|
+
// unknown feature and unrelated byte outside them is verified unchanged.
|
|
929
|
+
const edits = [
|
|
930
|
+
{ flagId: 'host.workbuddy', enabled: !hostExplicitOff },
|
|
931
|
+
{ flagId: 'workbuddy_live_keyword_pain', enabled: false },
|
|
932
|
+
];
|
|
933
|
+
let next = rawText;
|
|
934
|
+
for (const edit of edits) {
|
|
935
|
+
const outcome = applyFeatureFlagLineEdit(next, {
|
|
936
|
+
flagId: edit.flagId,
|
|
937
|
+
enabled: edit.enabled,
|
|
938
|
+
category: WORKBUDDY_FLAG_CATEGORY,
|
|
939
|
+
configPath,
|
|
940
|
+
});
|
|
941
|
+
if (!outcome.ok) {
|
|
942
|
+
return { ok: false, reason: outcome.reason, nextAction: outcome.nextAction, hostExplicitOff, keywordExplicitOff, keywordPainEnabled: false, changed: false };
|
|
943
|
+
}
|
|
944
|
+
({ next } = outcome);
|
|
945
|
+
}
|
|
946
|
+
// The summary must report the gate that the config ACTUALLY holds, not the
|
|
947
|
+
// value we intended to write. `applyFeatureFlagLineEdit` has already parsed
|
|
948
|
+
// `next` successfully, so this read only feeds the summary.
|
|
949
|
+
const finalParsed = yaml.load(next);
|
|
950
|
+
const finalFeatures = isRecord(finalParsed) ? finalParsed.features : undefined;
|
|
951
|
+
const finalKeyword = isRecord(finalFeatures) ? finalFeatures.workbuddy_live_keyword_pain : undefined;
|
|
952
|
+
const keywordPainEnabled = isRecord(finalKeyword) && finalKeyword.enabled === true;
|
|
582
953
|
try {
|
|
583
|
-
const verify = yaml.load(next);
|
|
584
|
-
if (!isRecord(verify) || !isRecord(verify.features) ||
|
|
585
|
-
!isRecord(verify.features['host.workbuddy']) || verify.features['host.workbuddy'].enabled !== !hostExplicitOff ||
|
|
586
|
-
!isRecord(verify.features.workbuddy_live_keyword_pain) || verify.features.workbuddy_live_keyword_pain.enabled !== !keywordExplicitOff) {
|
|
587
|
-
return { ok: false, reason: 'fresh_workbuddy_flags_verification_failed', nextAction: 'No changes written.', hostExplicitOff, keywordExplicitOff, changed: false };
|
|
588
|
-
}
|
|
589
954
|
atomicReplaceTextFile(configPath, next);
|
|
590
955
|
}
|
|
591
956
|
catch {
|
|
592
|
-
return { ok: false, reason: 'fresh_workbuddy_flag_write_failed', nextAction: 'Repair config storage and retry.', hostExplicitOff, keywordExplicitOff, changed: false };
|
|
957
|
+
return { ok: false, reason: 'fresh_workbuddy_flag_write_failed', nextAction: 'Repair config storage and retry.', hostExplicitOff, keywordExplicitOff, keywordPainEnabled, changed: false };
|
|
593
958
|
}
|
|
594
|
-
return { ok: true, configPath, enabled: !hostExplicitOff, hostExplicitOff, keywordExplicitOff, changed: true };
|
|
959
|
+
return { ok: true, configPath, enabled: !hostExplicitOff, hostExplicitOff, keywordExplicitOff, keywordPainEnabled, changed: true };
|
|
595
960
|
}
|
|
596
961
|
/**
|
|
597
|
-
*
|
|
962
|
+
* The single plugin-state judgment: registration (registry) and enablement
|
|
963
|
+
* (settings) are INDEPENDENT facts and neither may be inferred from the other
|
|
964
|
+
* (OD-03). Extracted so the install path and the read-only `status` path share
|
|
965
|
+
* exactly one implementation.
|
|
966
|
+
*/
|
|
967
|
+
export function readWorkbuddyPluginState(p) {
|
|
968
|
+
const registry = readHostJsonFile(p.registryPath);
|
|
969
|
+
const registered = registry.ok && registry.existed && isRecord(registry.value.plugins)
|
|
970
|
+
? Object.hasOwn(registry.value.plugins, WORKBUDDY_PLUGIN_KEY)
|
|
971
|
+
: false;
|
|
972
|
+
const settings = readHostJsonFile(p.settingsPath);
|
|
973
|
+
const enabledPlugins = settings.ok && isRecord(settings.value?.enabledPlugins) ? settings.value.enabledPlugins : undefined;
|
|
974
|
+
const raw = enabledPlugins === undefined ? undefined : enabledPlugins[WORKBUDDY_PLUGIN_KEY];
|
|
975
|
+
return {
|
|
976
|
+
registered,
|
|
977
|
+
registryReadable: registry.ok,
|
|
978
|
+
enabled: typeof raw === 'boolean' ? raw : null,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Read-only host state projection for the installer's `status` command.
|
|
598
983
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
|
|
984
|
+
* It reports the SAME three independent facts the install summary reports —
|
|
985
|
+
* registry presence, enablement, governance gates — and is rendered by the
|
|
986
|
+
* SAME authority (`formatHostStateSummary`). It never writes: an unreadable
|
|
987
|
+
* source becomes UNKNOWN/UNREADABLE, never a guessed value.
|
|
988
|
+
*/
|
|
989
|
+
export function readWorkbuddyHostState(homeDir) {
|
|
990
|
+
const p = resolveWorkbuddyPaths(homeDir);
|
|
991
|
+
const pluginState = readWorkbuddyPluginState(p);
|
|
992
|
+
const governance = ensureWorkbuddyGovernanceEnabled(p.governanceDir, { persist: false });
|
|
993
|
+
if (!governance.ok) {
|
|
994
|
+
const configPath = path.join(p.governanceDir, '.pd', 'config.yaml');
|
|
995
|
+
return {
|
|
996
|
+
success: true,
|
|
997
|
+
hostId: 'workbuddy',
|
|
998
|
+
pluginState,
|
|
999
|
+
governance: {
|
|
1000
|
+
enabled: false,
|
|
1001
|
+
explicitOptOut: false,
|
|
1002
|
+
configPath,
|
|
1003
|
+
readable: false,
|
|
1004
|
+
unreadableReason: `${governance.reason} (${configPath})`,
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
success: true,
|
|
1010
|
+
hostId: 'workbuddy',
|
|
1011
|
+
pluginState,
|
|
1012
|
+
governance: {
|
|
1013
|
+
enabled: governance.enabled,
|
|
1014
|
+
explicitOptOut: governance.hostExplicitOff,
|
|
1015
|
+
keywordPainEnabled: governance.keywordPainEnabled,
|
|
1016
|
+
configPath: governance.configPath,
|
|
1017
|
+
readable: true,
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* PRI-1064 review: the install summary must state the governance gate
|
|
1023
|
+
* truthfully.
|
|
604
1024
|
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
1025
|
+
* 2026-11-05 (Owner decision): WorkBuddy has no persisted observation writer,
|
|
1026
|
+
* so automatic Pain detection is UNSUPPORTED there — every keyword candidate
|
|
1027
|
+
* ends as `observation_only` and is never persisted. The summary therefore
|
|
1028
|
+
* NEVER reports keyword Pain as ON; it states the actual intake path (the
|
|
1029
|
+
* explicit `PD记录痛苦:` prefix) instead. Reporting the keyword gate as ON was
|
|
1030
|
+
* a false capability claim of exactly the kind this change set out to remove.
|
|
608
1031
|
*/
|
|
609
1032
|
export function describeGovernanceDefaults(state, governanceDir) {
|
|
610
1033
|
if (state === null) {
|
|
611
1034
|
return ` Governance flag state is UNKNOWN — no governance config exists at ${governanceDir} (run "pd runtime init --workspace ${governanceDir}", then re-run the installer).`;
|
|
612
1035
|
}
|
|
1036
|
+
const manualIntake = 'Automatic Pain detection is not available in WorkBuddy; record pain with the explicit prefix "PD记录痛苦:".';
|
|
613
1037
|
if (state.hostExplicitOff) {
|
|
614
|
-
return
|
|
1038
|
+
return ` WorkBuddy governance is OFF — your explicit opt-out (features.host.workbuddy.enabled=false) was preserved; four-term keyword Pain detection is OFF${state.keywordExplicitOff ? ' (the OFF setting was preserved)' : ' (automatic detection is unavailable in WorkBuddy, so it is kept OFF)'}. ${manualIntake}`;
|
|
615
1039
|
}
|
|
616
|
-
const hostState = ` WorkBuddy governance is ON${state.changed ? ' (enabled by this install)' : ' (already enabled)'}`;
|
|
1040
|
+
const hostState = ` WorkBuddy governance is ON${state.changed ? ' (enabled by this install)' : ' (already enabled)'} — this flag only acts through the plugin/hook channel, so it has no effect until the plugin is enabled in WorkBuddy`;
|
|
617
1041
|
const keywordState = state.keywordExplicitOff
|
|
618
|
-
? 'four-term keyword Pain detection is OFF —
|
|
619
|
-
: 'four-term keyword Pain detection is
|
|
620
|
-
return `${hostState}; ${keywordState}
|
|
1042
|
+
? 'four-term keyword Pain detection is OFF — the OFF setting was preserved (an explicit Owner opt-out is never overridden)'
|
|
1043
|
+
: 'four-term keyword Pain detection is OFF — automatic detection is unavailable in WorkBuddy, so it is kept OFF';
|
|
1044
|
+
return `${hostState}; ${keywordState}. ${manualIntake}`;
|
|
621
1045
|
}
|
|
622
|
-
|
|
1046
|
+
/**
|
|
1047
|
+
* Generic line-targeted flag editor (flag id + registry category are
|
|
1048
|
+
* parameters). Writes only the flag's `enabled`/`category` lines, keeps every
|
|
1049
|
+
* other byte intact, and fails closed when the block cannot be located safely.
|
|
1050
|
+
*/
|
|
1051
|
+
export function setWorkbuddyFeatureFlag(governanceDir, flag) {
|
|
1052
|
+
const { flagId, enabled, category } = flag;
|
|
623
1053
|
const configDir = path.join(governanceDir, '.pd');
|
|
624
1054
|
const configPath = path.join(configDir, 'config.yaml');
|
|
625
1055
|
let raw;
|
|
@@ -638,125 +1068,28 @@ export function setWorkbuddyGovernanceFlag(governanceDir, enabled) {
|
|
|
638
1068
|
return {
|
|
639
1069
|
ok: false,
|
|
640
1070
|
reason: `config_unparsable: ${configPath} is not valid YAML (${error instanceof Error ? error.message : String(error)}). No write was performed.`,
|
|
641
|
-
nextAction: 'Fix or delete the malformed config.yaml, then retry
|
|
1071
|
+
nextAction: 'Fix or delete the malformed config.yaml by hand, then retry.',
|
|
642
1072
|
};
|
|
643
1073
|
}
|
|
644
1074
|
}
|
|
645
1075
|
else {
|
|
646
1076
|
raw = generateConfigYamlContent();
|
|
647
1077
|
}
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const line = lines[i];
|
|
652
|
-
if (typeof line === 'string' && WORKBUDDY_KEY_LINE.test(line)) {
|
|
653
|
-
keyIndex = i;
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (keyIndex >= 0) {
|
|
658
|
-
// Remediation (review 2026-10-13 §3 P2 + round-3 P2): locate the existing
|
|
659
|
-
// `enabled` and `category` fields WITHIN the flag's mapping block. The
|
|
660
|
-
// block boundary is INDENTATION, not field identity: it spans every
|
|
661
|
-
// non-blank line indented ≥ 4 spaces after the 2-space `host.workbuddy:`
|
|
662
|
-
// key, plus the comments/blank lines interleaved with them, and ends at
|
|
663
|
-
// the first structural line indented < 4 spaces (dedent / next key) or
|
|
664
|
-
// EOF. Legal entry fields other than the two we manage — e.g. `source:`
|
|
665
|
-
// (PRI-637 provenance, FeatureFlagEntry source?: 'owner' | 'system') —
|
|
666
|
-
// live INSIDE the block and must NEVER end the scan: the previous
|
|
667
|
-
// first-unrecognized-line break mistook `source: owner` for the block end
|
|
668
|
-
// and spliced duplicate keys above it, so the rewrite verification threw
|
|
669
|
-
// (config_rewrite_invalid) and enable/disable was refused with the stale
|
|
670
|
-
// value preserved.
|
|
671
|
-
let enabledIndex = -1;
|
|
672
|
-
let categoryIndex = -1;
|
|
673
|
-
for (let i = keyIndex + 1; i < lines.length; i++) {
|
|
674
|
-
const line = lines[i];
|
|
675
|
-
if (typeof line !== 'string')
|
|
676
|
-
break; // EOF — block ended
|
|
677
|
-
if (line.trim() === '' || /^\s*#/.test(line))
|
|
678
|
-
continue; // blank / comment — transparent, inside the block
|
|
679
|
-
if (!/^ {4}/.test(line))
|
|
680
|
-
break; // dedent — mapping entry ended
|
|
681
|
-
if (WORKBUDDY_ENABLED_LINE.test(line)) {
|
|
682
|
-
enabledIndex = i;
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
if (WORKBUDDY_CATEGORY_LINE.test(line)) {
|
|
686
|
-
categoryIndex = i;
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
// Any other legal field (source: …) or nested content — inside the
|
|
690
|
-
// block: keep scanning, never touch it.
|
|
691
|
-
}
|
|
692
|
-
if (enabledIndex >= 0) {
|
|
693
|
-
// In-place flip; nothing else in the block moves.
|
|
694
|
-
lines[enabledIndex] = ` enabled: ${enabled}`;
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
// No enabled field anywhere in the block: insert it under the key line.
|
|
698
|
-
lines.splice(keyIndex + 1, 0, ` enabled: ${enabled}`);
|
|
699
|
-
if (categoryIndex > keyIndex)
|
|
700
|
-
categoryIndex += 1;
|
|
701
|
-
}
|
|
702
|
-
// R1 write contract: EVERY entry this function writes carries the
|
|
703
|
-
// schema-required category (production validator requirement). Insert it
|
|
704
|
-
// only when the block has none — never alongside an existing one.
|
|
705
|
-
if (categoryIndex >= 0) {
|
|
706
|
-
lines[categoryIndex] = ` category: ${WORKBUDDY_FLAG_CATEGORY}`;
|
|
707
|
-
}
|
|
708
|
-
else if (enabledIndex >= 0) {
|
|
709
|
-
lines.splice(enabledIndex + 1, 0, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
lines.splice(keyIndex + 2, 0, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
// Match BOTH an existing mapping header (`features:`) and the empty flow
|
|
717
|
-
// mapping the installer template writes (`features: {}`) — appending a
|
|
718
|
-
// SECOND features block instead would create a duplicate YAML key.
|
|
719
|
-
const featuresIndex = lines.findIndex((line) => FEATURES_LINE.test(line) || FEATURES_EMPTY_MAPPING_LINE.test(line));
|
|
720
|
-
if (featuresIndex >= 0) {
|
|
721
|
-
const featuresLine = lines[featuresIndex];
|
|
722
|
-
if (typeof featuresLine === 'string' && isEmptyFeaturesMapping(featuresLine)) {
|
|
723
|
-
lines[featuresIndex] = 'features:';
|
|
724
|
-
}
|
|
725
|
-
lines.splice(featuresIndex + 1, 0, ` host.workbuddy:`, ` enabled: ${enabled}`, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
|
|
726
|
-
}
|
|
727
|
-
else {
|
|
728
|
-
// No features: mapping at all — append a block at the end.
|
|
729
|
-
lines.push('features:', ` host.workbuddy:`, ` enabled: ${enabled}`, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const next = lines.join('\n');
|
|
733
|
-
// Round-trip verification: the rewritten config must parse with the flag at
|
|
734
|
-
// the intended value AND the registry-required category before it is
|
|
735
|
-
// committed (same discipline as codex-setup).
|
|
736
|
-
let verify;
|
|
737
|
-
try {
|
|
738
|
-
verify = yaml.load(next);
|
|
739
|
-
}
|
|
740
|
-
catch (error) {
|
|
741
|
-
return { ok: false, reason: `config_rewrite_invalid: verification parse failed (${error instanceof Error ? error.message : String(error)}). Nothing was written.`, nextAction: 'Report this failure — the flag edit produced invalid YAML.' };
|
|
742
|
-
}
|
|
743
|
-
const flagOk = isRecord(verify)
|
|
744
|
-
&& isRecord(verify.features)
|
|
745
|
-
&& isRecord(verify.features['host.workbuddy'])
|
|
746
|
-
&& verify.features['host.workbuddy'].enabled === enabled
|
|
747
|
-
&& verify.features['host.workbuddy'].category === WORKBUDDY_FLAG_CATEGORY;
|
|
748
|
-
if (!flagOk) {
|
|
749
|
-
return { ok: false, reason: 'config_rewrite_mismatch: round-trip verification did not observe the intended flag value + category. Nothing was written.', nextAction: 'Report this failure with the config.yaml content.' };
|
|
750
|
-
}
|
|
1078
|
+
const outcome = applyFeatureFlagLineEdit(raw, { flagId, enabled, category, configPath });
|
|
1079
|
+
if (!outcome.ok)
|
|
1080
|
+
return outcome;
|
|
751
1081
|
try {
|
|
752
1082
|
mkdirSync(configDir, { recursive: true });
|
|
753
|
-
atomicReplaceTextFile(configPath, next);
|
|
1083
|
+
atomicReplaceTextFile(configPath, outcome.next);
|
|
754
1084
|
}
|
|
755
1085
|
catch (error) {
|
|
756
1086
|
return { ok: false, reason: `config_write_failed: ${configPath} (${error instanceof Error ? error.message : String(error)})`, nextAction: 'Fix permissions/free the file and retry.' };
|
|
757
1087
|
}
|
|
758
1088
|
return { ok: true, configPath, enabled };
|
|
759
1089
|
}
|
|
1090
|
+
export function setWorkbuddyGovernanceFlag(governanceDir, enabled) {
|
|
1091
|
+
return setWorkbuddyFeatureFlag(governanceDir, { flagId: 'host.workbuddy', enabled, category: WORKBUDDY_FLAG_CATEGORY });
|
|
1092
|
+
}
|
|
760
1093
|
/** Read the explicit host.workbuddy override state (null = no explicit override → registry default OFF). */
|
|
761
1094
|
export function readWorkbuddyGovernanceFlagOverride(governanceDir) {
|
|
762
1095
|
const configPath = path.join(governanceDir, '.pd', 'config.yaml');
|
|
@@ -1345,7 +1678,7 @@ export class WorkBuddyHostInstaller {
|
|
|
1345
1678
|
// unaffected when the MCP component is missing or the write fails.
|
|
1346
1679
|
const mcpBinding = writeWorkbuddyMcpBinding(p.marketplaceRoot, this.deps.homeDir());
|
|
1347
1680
|
// 5. Register: engine commands first; observable direct-write fallback.
|
|
1348
|
-
const registration = this.registerPlugin(details, payload.version);
|
|
1681
|
+
const registration = this.registerPlugin(details, payload.version, payload.defaultEnabled);
|
|
1349
1682
|
if (!registration.ok) {
|
|
1350
1683
|
return {
|
|
1351
1684
|
success: false,
|
|
@@ -1441,12 +1774,15 @@ export class WorkBuddyHostInstaller {
|
|
|
1441
1774
|
hostId: this.hostId,
|
|
1442
1775
|
configPath: p.settingsPath,
|
|
1443
1776
|
configAction: registration.settingsAction,
|
|
1777
|
+
pluginState: this.readInstalledPluginState(),
|
|
1444
1778
|
governance: governanceDefaults === null
|
|
1445
1779
|
? undefined
|
|
1446
1780
|
: {
|
|
1447
1781
|
enabled: !governanceDefaults.hostExplicitOff,
|
|
1448
1782
|
explicitOptOut: governanceDefaults.hostExplicitOff,
|
|
1449
|
-
|
|
1783
|
+
// Read back from the config after the migration (OFF by contract),
|
|
1784
|
+
// never inferred from the Owner opt-out flag.
|
|
1785
|
+
keywordPainEnabled: governanceDefaults.keywordPainEnabled,
|
|
1450
1786
|
configPath: governanceDefaults.configPath,
|
|
1451
1787
|
},
|
|
1452
1788
|
reason: `Registered ${WORKBUDDY_PLUGIN_KEY} via ${registration.mode}${registration.mode === 'engine' ? '' : ` (engine unreachable: ${registration.engineNote})`}. Governance workspace initialized at ${p.governanceDir}${governance.dbInit === 'skipped' ? ` (${governance.note})` : ''}.${profileNote}${governanceDefaultsNote}${mcpBinding.ok ? ' pd-recall MCP binding written.' : ` pd-recall MCP binding skipped (${mcpBinding.reason ?? 'unknown'}).`}`,
|
|
@@ -1454,7 +1790,7 @@ export class WorkBuddyHostInstaller {
|
|
|
1454
1790
|
// main; the "Existing workspaces retain their prior settings" sentence is
|
|
1455
1791
|
// deliberately dropped — PRI-1064 turns governance ON for those
|
|
1456
1792
|
// workspaces, so keeping it would state the opposite of what happens.
|
|
1457
|
-
nextAction: `${effectiveNote}${governance.dbInit === 'skipped' ? ` After the install, run "pd runtime init --workspace ${p.governanceDir}" to create the governance databases.` : ''} To disable, edit ${path.join(p.governanceDir, '.pd', 'config.yaml')} — each flag id is ONE flat key (not nested). In the existing "features:" block set
|
|
1793
|
+
nextAction: `${effectiveNote}${governance.dbInit === 'skipped' ? ` After the install, run "pd runtime init --workspace ${p.governanceDir}" to create the governance databases.` : ''} To disable, edit ${path.join(p.governanceDir, '.pd', 'config.yaml')} — each flag id is ONE flat key (not nested). In the existing "features:" block set host.workbuddy: {enabled: false} to disable all WorkBuddy governance. Automatic Pain detection is unavailable in WorkBuddy; record pain only through the explicit "PD记录痛苦:" prefix.${pendingRebindNote}`,
|
|
1458
1794
|
};
|
|
1459
1795
|
}
|
|
1460
1796
|
/**
|
|
@@ -1531,20 +1867,32 @@ export class WorkBuddyHostInstaller {
|
|
|
1531
1867
|
note: `governance database initialization SKIPPED: pd-cli runtime entry not found — run "pd runtime init --workspace ${governanceDir}" after the install; until then pain evidence degrades with an explicit trajectory_db_not_found note.`,
|
|
1532
1868
|
};
|
|
1533
1869
|
}
|
|
1534
|
-
registerPlugin(details, version) {
|
|
1870
|
+
registerPlugin(details, version, declaredDefaultEnabled) {
|
|
1535
1871
|
const p = this.paths();
|
|
1536
1872
|
// A registered old plugin can still be hook-only, even at the same
|
|
1537
1873
|
// manifest version. Reuse the installer-owned registration/cache writer
|
|
1538
1874
|
// rather than reporting a stale cache as an idempotent successful install.
|
|
1539
1875
|
if (details.registered && !this.pluginCacheMatchesPayload(version)) {
|
|
1540
|
-
return this.directWriteRegistration(details, version,
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1876
|
+
return this.directWriteRegistration(details, version, {
|
|
1877
|
+
declaredDefaultEnabled,
|
|
1878
|
+
engineNote: 'registered plugin cache differs from the current bundled payload; refreshed by installer',
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
// Idempotent fast path: already registered AND cached. The installed
|
|
1882
|
+
// enabled state is the OWNER's choice — install/upgrade/reinstall must not
|
|
1883
|
+
// flip it in either direction (install ≠ enable, SPEC §6.4).
|
|
1884
|
+
if (details.registered && existsSync(p.cachePluginRoot)) {
|
|
1544
1885
|
return { ok: true, mode: 'already-registered', settingsAction: 'preserved' };
|
|
1545
1886
|
}
|
|
1546
1887
|
// Engine path (preferred — real marketplace/cache/settings chain).
|
|
1547
1888
|
if (details.engineFound) {
|
|
1889
|
+
// Resolve the installed enabled value BEFORE the engine runs: an engine
|
|
1890
|
+
// install must not enable the plugin, and a pre-existing Owner choice
|
|
1891
|
+
// (true or false) must survive the install.
|
|
1892
|
+
const installedEnabled = this.resolveInstalledEnabledValue(declaredDefaultEnabled);
|
|
1893
|
+
if (!installedEnabled.ok) {
|
|
1894
|
+
return { ok: false, reason: installedEnabled.reason, nextAction: installedEnabled.nextAction };
|
|
1895
|
+
}
|
|
1548
1896
|
const add = this.runEngine(['plugin', 'marketplace', 'add', p.marketplaceRoot]);
|
|
1549
1897
|
if (add !== undefined && add.status === 0 && MARKETPLACE_ADD_OK.test(add.stdout + add.stderr)) {
|
|
1550
1898
|
if (details.registered) {
|
|
@@ -1554,28 +1902,26 @@ export class WorkBuddyHostInstaller {
|
|
|
1554
1902
|
}
|
|
1555
1903
|
const install = this.runEngine(['plugin', 'install', WORKBUDDY_PLUGIN_KEY]);
|
|
1556
1904
|
if (install !== undefined && install.status === 0 && PLUGIN_INSTALL_OK.test(install.stdout + install.stderr)) {
|
|
1557
|
-
|
|
1558
|
-
if (after.registered
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
// OFF either way (OD-03: features.host.workbuddy is a separate,
|
|
1566
|
-
// later, user-visible enable).
|
|
1567
|
-
const enabled = this.enableRegisteredPlugin();
|
|
1568
|
-
if (!enabled.ok) {
|
|
1569
|
-
return { ok: false, reason: enabled.reason, nextAction: enabled.nextAction };
|
|
1905
|
+
const after = this.detectDetails();
|
|
1906
|
+
if (after.registered) {
|
|
1907
|
+
// Converge the settings entry onto the resolved value. This covers
|
|
1908
|
+
// engines that would force-enable on install while still preserving
|
|
1909
|
+
// an Owner-chosen true/false from before the install.
|
|
1910
|
+
const applied = this.applyInstalledEnabledValue(installedEnabled.value);
|
|
1911
|
+
if (!applied.ok) {
|
|
1912
|
+
return { ok: false, reason: applied.reason, nextAction: applied.nextAction };
|
|
1570
1913
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1914
|
+
return {
|
|
1915
|
+
ok: true,
|
|
1916
|
+
mode: 'engine',
|
|
1917
|
+
settingsAction: details.settingsExisted
|
|
1918
|
+
? (installedEnabled.fromOwnerChoice ? 'preserved' : 'updated')
|
|
1919
|
+
: 'created',
|
|
1920
|
+
};
|
|
1575
1921
|
}
|
|
1576
1922
|
return {
|
|
1577
1923
|
ok: false,
|
|
1578
|
-
reason: `workbuddy_engine_install_unverified: engine reported success but
|
|
1924
|
+
reason: `workbuddy_engine_install_unverified: engine reported success but installed_plugins.json does not carry ${WORKBUDDY_PLUGIN_KEY} afterwards.`,
|
|
1579
1925
|
nextAction: `Run "node <engine> plugin list" to inspect the host state, then re-run the installer.`,
|
|
1580
1926
|
};
|
|
1581
1927
|
}
|
|
@@ -1590,14 +1936,17 @@ export class WorkBuddyHostInstaller {
|
|
|
1590
1936
|
const note = add !== undefined
|
|
1591
1937
|
? `engine marketplace add failed: ${((add.stderr || add.stdout).trim().slice(0, 200) || `exit ${add.status}`)}`
|
|
1592
1938
|
: 'engine invocation failed';
|
|
1593
|
-
const direct = this.directWriteRegistration(details, version, note);
|
|
1939
|
+
const direct = this.directWriteRegistration(details, version, { declaredDefaultEnabled, engineNote: note });
|
|
1594
1940
|
if (direct.ok)
|
|
1595
1941
|
return direct;
|
|
1596
1942
|
return { ok: false, reason: direct.reason, nextAction: direct.nextAction };
|
|
1597
1943
|
}
|
|
1598
1944
|
// Engine unreachable: direct-write path, strictly aligned with the frozen
|
|
1599
1945
|
// formats, and always reported as a distinct mode.
|
|
1600
|
-
const direct = this.directWriteRegistration(details, version,
|
|
1946
|
+
const direct = this.directWriteRegistration(details, version, {
|
|
1947
|
+
declaredDefaultEnabled,
|
|
1948
|
+
engineNote: 'engine binary not found',
|
|
1949
|
+
});
|
|
1601
1950
|
if (direct.ok)
|
|
1602
1951
|
return direct;
|
|
1603
1952
|
return { ok: false, reason: direct.reason, nextAction: direct.nextAction };
|
|
@@ -1626,33 +1975,63 @@ export class WorkBuddyHostInstaller {
|
|
|
1626
1975
|
}
|
|
1627
1976
|
}
|
|
1628
1977
|
/**
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
* defaultEnabled) onto the installer's frozen installed ⇒ enabled contract.
|
|
1978
|
+
* The value an install should leave behind: an explicit boolean already in
|
|
1979
|
+
* settings.json is the Owner's choice and always wins; otherwise the plugin
|
|
1980
|
+
* manifest's `defaultEnabled` applies. Install never enables by itself.
|
|
1633
1981
|
*/
|
|
1634
|
-
|
|
1982
|
+
resolveInstalledEnabledValue(declaredDefaultEnabled) {
|
|
1635
1983
|
const p = this.paths();
|
|
1636
1984
|
const settings = readHostJsonFile(p.settingsPath);
|
|
1637
1985
|
if (!settings.ok)
|
|
1638
1986
|
return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
|
|
1987
|
+
const current = isRecord(settings.value?.enabledPlugins)
|
|
1988
|
+
? settings.value.enabledPlugins[WORKBUDDY_PLUGIN_KEY]
|
|
1989
|
+
: undefined;
|
|
1990
|
+
if (typeof current === 'boolean')
|
|
1991
|
+
return { ok: true, value: current, fromOwnerChoice: true };
|
|
1992
|
+
return { ok: true, value: declaredDefaultEnabled, fromOwnerChoice: false };
|
|
1993
|
+
}
|
|
1994
|
+
/** Write enabledPlugins[OUR_KEY]=value, preserving every other key. */
|
|
1995
|
+
applyInstalledEnabledValue(value) {
|
|
1996
|
+
const p = this.paths();
|
|
1997
|
+
const settings = readHostJsonFile(p.settingsPath);
|
|
1998
|
+
if (!settings.ok)
|
|
1999
|
+
return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
|
|
2000
|
+
const current = isRecord(settings.value?.enabledPlugins)
|
|
2001
|
+
? settings.value.enabledPlugins[WORKBUDDY_PLUGIN_KEY]
|
|
2002
|
+
: undefined;
|
|
2003
|
+
if (typeof current === 'boolean' && current === value)
|
|
2004
|
+
return { ok: true, changed: false };
|
|
1639
2005
|
const nextSettings = { ...(settings.value ?? {}) };
|
|
1640
2006
|
const enabledPlugins = isRecord(nextSettings.enabledPlugins) ? { ...nextSettings.enabledPlugins } : {};
|
|
1641
|
-
enabledPlugins[WORKBUDDY_PLUGIN_KEY] =
|
|
2007
|
+
enabledPlugins[WORKBUDDY_PLUGIN_KEY] = value;
|
|
1642
2008
|
nextSettings.enabledPlugins = enabledPlugins;
|
|
1643
2009
|
const write = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
|
|
1644
2010
|
if (!write.ok)
|
|
1645
2011
|
return { ok: false, reason: write.reason, nextAction: write.nextAction };
|
|
1646
|
-
return { ok: true };
|
|
2012
|
+
return { ok: true, changed: true };
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Read the two INDEPENDENT post-install facts straight from the host:
|
|
2016
|
+
* - registration → installed_plugins.json (host registry);
|
|
2017
|
+
* - enablement → settings.json enabledPlugins[our key].
|
|
2018
|
+
* Neither may be inferred from the other, from the governance flag, or from
|
|
2019
|
+
* the engine's exit status. An unreadable registry must NOT be reported as
|
|
2020
|
+
* "installed" (rc-9).
|
|
2021
|
+
*/
|
|
2022
|
+
readInstalledPluginState() {
|
|
2023
|
+
return readWorkbuddyPluginState(this.paths());
|
|
1647
2024
|
}
|
|
1648
2025
|
/**
|
|
1649
2026
|
* Direct-write registration for engine-unreachable machines, aligned with the
|
|
1650
|
-
* frozen P01 formats: enabledPlugins[key]
|
|
1651
|
-
*
|
|
2027
|
+
* frozen P01 formats: enabledPlugins[key] carries the manifest default or the
|
|
2028
|
+
* Owner's existing choice (install never enables by itself; other keys and
|
|
2029
|
+
* other plugins preserved), installed_plugins.json v2 user registration
|
|
1652
2030
|
* (native scope array or legacy object), and the versioned cache
|
|
1653
2031
|
* copy under plugins/cache/<marketplace>/<plugin>/<version>/.
|
|
1654
2032
|
*/
|
|
1655
|
-
directWriteRegistration(details, version,
|
|
2033
|
+
directWriteRegistration(details, version, options) {
|
|
2034
|
+
const { declaredDefaultEnabled, engineNote } = options;
|
|
1656
2035
|
const p = this.paths();
|
|
1657
2036
|
const cacheVersionDir = workbuddyCacheVersionDir(p.cachePluginRoot, version);
|
|
1658
2037
|
if (cacheVersionDir === undefined) {
|
|
@@ -1679,9 +2058,15 @@ export class WorkBuddyHostInstaller {
|
|
|
1679
2058
|
}
|
|
1680
2059
|
// settings.json merge — computed here, WRITTEN after the cache lands (see
|
|
1681
2060
|
// below): a failed payload must leave no registration state behind.
|
|
2061
|
+
// Install ≠ enable (SPEC §6.4): an explicit Owner choice is preserved;
|
|
2062
|
+
// otherwise the manifest's defaultEnabled applies.
|
|
2063
|
+
const currentEnabled = isRecord(settings.value?.enabledPlugins)
|
|
2064
|
+
? settings.value.enabledPlugins[WORKBUDDY_PLUGIN_KEY]
|
|
2065
|
+
: undefined;
|
|
2066
|
+
const installedEnabled = typeof currentEnabled === 'boolean' ? currentEnabled : declaredDefaultEnabled;
|
|
1682
2067
|
const nextSettings = { ...(settings.value ?? {}) };
|
|
1683
2068
|
const enabledPlugins = isRecord(nextSettings.enabledPlugins) ? { ...nextSettings.enabledPlugins } : {};
|
|
1684
|
-
enabledPlugins[WORKBUDDY_PLUGIN_KEY] =
|
|
2069
|
+
enabledPlugins[WORKBUDDY_PLUGIN_KEY] = installedEnabled;
|
|
1685
2070
|
nextSettings.enabledPlugins = enabledPlugins;
|
|
1686
2071
|
const settingsAction = settings.existed ? 'updated' : 'created';
|
|
1687
2072
|
// installed_plugins.json v2 merge (create with version 2 when absent)
|