@event4u/agent-config 8.11.0 → 8.13.0

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.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +64 -266
  4. package/README.md +2 -2
  5. package/dist/agent-src/commands/pr/create.md +26 -0
  6. package/dist/agent-src/commands/team/adversarial.md +96 -0
  7. package/dist/agent-src/commands/team/delegate.md +109 -0
  8. package/dist/agent-src/commands/team/review.md +84 -0
  9. package/dist/agent-src/commands/team/status.md +92 -0
  10. package/dist/agent-src/commands/team.md +147 -0
  11. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  12. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
  13. package/dist/agent-src/rules/architecture.md +6 -51
  14. package/dist/agent-src/rules/code-comment-discipline.md +10 -94
  15. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  16. package/dist/agent-src/rules/improve-before-implement.md +8 -65
  17. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  18. package/dist/agent-src/rules/persona-governance.md +5 -47
  19. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  20. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  21. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  22. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
  23. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  24. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  25. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  26. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  27. package/dist/discovery/deprecation-report.md +1 -1
  28. package/dist/discovery/discovery-manifest.json +159 -21
  29. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  30. package/dist/discovery/discovery-manifest.summary.md +3 -3
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +11 -6
  33. package/dist/discovery/trust-report.md +2 -2
  34. package/dist/discovery/workspaces.json +10 -5
  35. package/dist/install/install.mjs +38 -0
  36. package/dist/mcp/registry-manifest.json +2 -2
  37. package/dist/router.json +1 -1
  38. package/dist/server/schemas/settings.js +7 -0
  39. package/dist/server/schemas/settings.js.map +1 -1
  40. package/docs/CLAIMS.md +31 -3
  41. package/docs/architecture.md +2 -2
  42. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  43. package/docs/benchmark.md +39 -0
  44. package/docs/catalog.md +10 -4
  45. package/docs/command-flows.md +6 -1
  46. package/docs/contracts/ai-council-config.md +9 -4
  47. package/docs/contracts/ai-team-config.md +191 -0
  48. package/docs/contracts/command-clusters.md +1 -0
  49. package/docs/contracts/persona-schema.md +49 -0
  50. package/docs/contracts/provider-lifecycle.md +29 -4
  51. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  52. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  53. package/docs/decisions/INDEX.md +2 -0
  54. package/docs/design/council-vs-solo-baseline.md +92 -0
  55. package/docs/design/utilization-window-criteria.md +51 -0
  56. package/docs/featured-skills.md +1 -1
  57. package/docs/getting-started-by-role.md +3 -1
  58. package/docs/getting-started.md +31 -1
  59. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  60. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  61. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  62. package/docs/guidelines/code-clarity.md +86 -1
  63. package/docs/maintainers/branch-protection.md +105 -0
  64. package/docs/maintainers/system-map.md +85 -0
  65. package/docs/proof.md +15 -4
  66. package/package.json +1 -1
  67. package/src/config/agent-settings.template.yml +49 -0
  68. package/src/scripts/_cli/cmd_doctor.ts +291 -1
  69. package/src/scripts/_lib/claude_plugin.ts +39 -4
  70. package/src/scripts/_lib/knowledge_global.ts +44 -0
  71. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  72. package/src/scripts/ai_council/orchestrator.ts +188 -1
  73. package/src/scripts/ai_team/config.ts +162 -0
  74. package/src/scripts/bench_persona_placebo.ts +308 -0
  75. package/src/scripts/bench_quality_rerun.ts +357 -0
  76. package/src/scripts/build_proof.ts +9 -0
  77. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  78. package/src/scripts/check_knowledge_cards.ts +33 -4
  79. package/src/scripts/check_portability.ts +1 -0
  80. package/src/scripts/complexity_report.ts +776 -0
  81. package/src/scripts/consumer_matrix.ts +251 -12
  82. package/src/scripts/council_cli.ts +56 -1
  83. package/src/scripts/explain_run.ts +593 -0
  84. package/src/scripts/install.ts +39 -0
  85. package/src/scripts/knowledge_global_cli.ts +234 -45
  86. package/src/scripts/rule_backlinks.ts +350 -0
  87. package/src/server/schemas/settings.ts +17 -0
@@ -29,7 +29,7 @@
29
29
  * scope · global-binary · claude-plugin · stale-orphans · manifest-integrity ·
30
30
  * lockfile-freshness · bridge-drift · mcp-mode · mcp-beta-readiness ·
31
31
  * offline-readiness · python-runtime · humanizer-runtime · tier-usage-readiness ·
32
- * council-cli ·
32
+ * council-cli · team ·
33
33
  * unsupported-combos · wizard-state.
34
34
  * Each emits a structured `{id, status, message, remedy}` record with
35
35
  * `status` ∈ `ok` / `warn` / `fail` / `skipped` (rendered
@@ -731,6 +731,7 @@ function relativeTo(p: string, base: string): string | null {
731
731
  const CHECK_IDS = [
732
732
  'scope',
733
733
  'global-binary',
734
+ 'git-identity',
734
735
  'claude-plugin',
735
736
  'claude-command-wrappers',
736
737
  'surface-state',
@@ -746,6 +747,7 @@ const CHECK_IDS = [
746
747
  'humanizer-runtime',
747
748
  'tier-usage-readiness',
748
749
  'council-cli',
750
+ 'team',
749
751
  'unsupported-combos',
750
752
  'wizard-state',
751
753
  'settings-review-pending',
@@ -755,6 +757,7 @@ const CHECK_IDS = [
755
757
  const GLOBAL_CHECK_IDS: ReadonlySet<string> = new Set([
756
758
  'scope',
757
759
  'global-binary',
760
+ 'git-identity',
758
761
  'claude-plugin',
759
762
  'claude-command-wrappers',
760
763
  'surface-state',
@@ -767,6 +770,7 @@ const GLOBAL_CHECK_IDS: ReadonlySet<string> = new Set([
767
770
  'humanizer-runtime',
768
771
  'tier-usage-readiness',
769
772
  'council-cli',
773
+ 'team',
770
774
  'wizard-state',
771
775
  'settings-review-pending',
772
776
  ]);
@@ -840,6 +844,97 @@ function _check_scope(project_root: string): Dict {
840
844
  };
841
845
  }
842
846
 
847
+ /** Placeholder git identities that must never author real history — the
848
+ * 8.11 feedback found 72/100 recent commits stamped `t <t@t.t>` because a
849
+ * repo-local config carried a throwaway identity. Reads config FILES (no
850
+ * shell-out): repo-local wins over global, worktree gitdirs resolved. */
851
+ const _PLACEHOLDER_EMAIL_RE = /^(t@t\.t|.*@example\.(com|org)|.*@localhost)$/i;
852
+ const _PLACEHOLDER_NAME_RE = /^(t|test|user|nobody|x)$/i;
853
+
854
+ export function _git_config_path(project_root: string): string | null {
855
+ const gd = path.join(project_root, '.git');
856
+ try {
857
+ const st = fs.statSync(gd);
858
+ if (st.isDirectory()) {
859
+ return path.join(gd, 'config');
860
+ }
861
+ // Worktree: `.git` is a file "gitdir: <path>".
862
+ const m = readText(gd).match(/^gitdir:\s*(.+)\s*$/m);
863
+ if (m === null) {
864
+ return null;
865
+ }
866
+ const gdPath = path.resolve(project_root, m[1]!.trim());
867
+ const commondirFile = path.join(gdPath, 'commondir');
868
+ if (pathExists(commondirFile)) {
869
+ const common = path.resolve(gdPath, readText(commondirFile).trim());
870
+ return path.join(common, 'config');
871
+ }
872
+ return path.join(gdPath, 'config');
873
+ } catch {
874
+ return null;
875
+ }
876
+ }
877
+
878
+ function _parse_git_user(configText: string): { name: string | null; email: string | null } {
879
+ let inUser = false;
880
+ let name: string | null = null;
881
+ let email: string | null = null;
882
+ for (const raw of configText.split('\n')) {
883
+ const line = raw.trim();
884
+ if (line.startsWith('[')) {
885
+ inUser = /^\[user\]/i.test(line);
886
+ continue;
887
+ }
888
+ if (!inUser) {
889
+ continue;
890
+ }
891
+ const m = line.match(/^(name|email)\s*=\s*(.+)$/i);
892
+ if (m !== null) {
893
+ if (m[1]!.toLowerCase() === 'name') name = m[2]!.trim();
894
+ else email = m[2]!.trim();
895
+ }
896
+ }
897
+ return { name, email };
898
+ }
899
+
900
+ export function _check_git_identity(project_root: string): Dict {
901
+ const cfgPath = _git_config_path(project_root);
902
+ if (cfgPath === null || !pathExists(cfgPath)) {
903
+ return { id: 'git-identity', status: 'skipped', message: 'not a git repository', remedy: '' };
904
+ }
905
+ let ident = _parse_git_user(readText(cfgPath));
906
+ if (ident.name === null && ident.email === null) {
907
+ const globalCfg = path.join(os.homedir(), '.gitconfig');
908
+ if (pathExists(globalCfg)) {
909
+ ident = _parse_git_user(readText(globalCfg));
910
+ }
911
+ }
912
+ if (ident.name === null && ident.email === null) {
913
+ return {
914
+ id: 'git-identity',
915
+ status: 'warn',
916
+ message: 'no git user identity configured (repo or global)',
917
+ remedy: 'git config user.name "<you>" && git config user.email "<you@host>"',
918
+ };
919
+ }
920
+ const badEmail = ident.email !== null && _PLACEHOLDER_EMAIL_RE.test(ident.email);
921
+ const badName = ident.name !== null && _PLACEHOLDER_NAME_RE.test(ident.name);
922
+ if (badEmail || badName) {
923
+ return {
924
+ id: 'git-identity',
925
+ status: 'warn',
926
+ message: `placeholder git identity configured: ${ident.name ?? '?'} <${ident.email ?? '?'}> — commits will be unattributable`,
927
+ remedy: 'git config user.name "<you>" && git config user.email "<you@host>" (and add a .mailmap for already-authored history)',
928
+ };
929
+ }
930
+ return {
931
+ id: 'git-identity',
932
+ status: 'ok',
933
+ message: `${ident.name ?? '?'} <${ident.email ?? '?'}>`,
934
+ remedy: '',
935
+ };
936
+ }
937
+
843
938
  function _check_manifest_integrity(manifest: Dict): Dict {
844
939
  const schema = manifest['schema_version'];
845
940
  const version = manifest['agent_config_version'];
@@ -1882,6 +1977,196 @@ function pyFloat(value: number): string {
1882
1977
  return String(value);
1883
1978
  }
1884
1979
 
1980
+ // ---------------------------------------------------------------------------
1981
+ // team check (road-to-team-mode Phase 1) — codex binary + auth, official
1982
+ // codex plugin on Claude-Code hosts, Review-Gate consistency.
1983
+ // ---------------------------------------------------------------------------
1984
+
1985
+ /**
1986
+ * Codex CLI home — `CODEX_HOME` env override (the codex CLI's own
1987
+ * convention), else `~/.codex`.
1988
+ */
1989
+ function _codex_home(): string {
1990
+ const env = (process.env['CODEX_HOME'] ?? '').trim();
1991
+ return env !== '' ? env : path.join(os.homedir(), '.codex');
1992
+ }
1993
+
1994
+ /**
1995
+ * Codex binary name for the team check — REUSES the council probe's binary
1996
+ * discovery: when a council config exists and declares an enabled cli-mode
1997
+ * `openai` member with a `binary` override, honour it; else the provider
1998
+ * default from `_CLI_PROVIDER_META` (`codex`). Never throws — an unreadable
1999
+ * or invalid council config falls back to the default binary (the council-cli
2000
+ * check owns reporting that problem).
2001
+ */
2002
+ function _team_codex_binary_name(project_root: string): string {
2003
+ const default_bin = (_CLI_PROVIDER_META['openai'] as [string, boolean])[0];
2004
+ const { load_council_config, resolve_config_path } = ai_council_config;
2005
+ const council_path = resolve_config_path(project_root);
2006
+ if (!pathExists(String(council_path))) {
2007
+ return default_bin;
2008
+ }
2009
+ let cfg: ai_council_config.CouncilConfig;
2010
+ try {
2011
+ cfg = load_council_config(council_path);
2012
+ } catch {
2013
+ return default_bin;
2014
+ }
2015
+ const member = cfg.members.get('openai');
2016
+ if (member && member.enabled && member.mode === 'cli' && member.binary) {
2017
+ return member.binary;
2018
+ }
2019
+ return default_bin;
2020
+ }
2021
+
2022
+ /** Settings-flag coercion shared with the tier-usage check (bool or string). */
2023
+ function _settings_flag(val: unknown): boolean {
2024
+ if (typeof val === 'boolean') return val;
2025
+ if (typeof val === 'string') {
2026
+ return ['true', 'yes', 'on', '1'].includes(val.trim().toLowerCase());
2027
+ }
2028
+ return false;
2029
+ }
2030
+
2031
+ /** The `ai_team` block from `.agent-settings.yml` — lenient, `{}` when absent. */
2032
+ function _read_ai_team_block(project_root: string): Dict {
2033
+ const settings_file = project_settings_path(project_root);
2034
+ if (!isFile(settings_file)) return {};
2035
+ let content: string;
2036
+ try {
2037
+ content = readText0(settings_file);
2038
+ } catch {
2039
+ return {};
2040
+ }
2041
+ const loaded = yamlSafeLoad(content);
2042
+ if (typeof loaded !== 'object' || loaded === null || Array.isArray(loaded)) return {};
2043
+ const raw = (loaded as Dict)['ai_team'];
2044
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {};
2045
+ return raw as Dict;
2046
+ }
2047
+
2048
+ /**
2049
+ * `team` health check — three sub-signals folded into one result:
2050
+ *
2051
+ * (a) codex binary present (council-probe binary discovery reused) AND
2052
+ * auth present. Auth is a READ-ONLY presence probe of
2053
+ * `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`) — token
2054
+ * expiry cannot be validated without a billable call; runtime expiry
2055
+ * is caught by `OpenAICliClient._AUTH_FAILURE_PATTERNS`.
2056
+ * (b) on Claude-Code hosts (claude binary on PATH or the Claude config dir
2057
+ * present): the official codex plugin recorded in
2058
+ * `plugins/installed_plugins.json`. Detection is read-only — doctor
2059
+ * never writes under `~/.claude/`.
2060
+ * (c) Review-Gate consistency: `ai_team.review_gate.managed` on while the
2061
+ * loop bound (`max_consecutive_blocks`, Phase 4 — unbuilt) is absent
2062
+ * → WARN.
2063
+ *
2064
+ * Absent feature ≠ broken feature: with `ai_team` absent or disabled the
2065
+ * check reports ok ("team mode not configured (default-off)"); WARN fires
2066
+ * only on failing sub-signals or inconsistent half-configured states.
2067
+ */
2068
+ function _check_team(project_root: string): Dict {
2069
+ const ai_team = _read_ai_team_block(project_root);
2070
+ const enabled = _settings_flag(ai_team['enabled']);
2071
+ const gateRaw = ai_team['review_gate'];
2072
+ const gate =
2073
+ typeof gateRaw === 'object' && gateRaw !== null && !Array.isArray(gateRaw)
2074
+ ? (gateRaw as Dict)
2075
+ : {};
2076
+ const gate_managed = _settings_flag(gate['managed']);
2077
+ const bound = gate['max_consecutive_blocks'];
2078
+ const bound_present = typeof bound === 'number' && Number.isInteger(bound) && bound > 0;
2079
+
2080
+ if (!enabled) {
2081
+ if (gate_managed) {
2082
+ return {
2083
+ id: 'team',
2084
+ status: 'warn',
2085
+ message:
2086
+ 'half-configured: ai_team.review_gate.managed is on but ' +
2087
+ 'ai_team.enabled is false — the gate never runs',
2088
+ remedy:
2089
+ 'set `ai_team.enabled: true` in .agent-settings.yml, or ' +
2090
+ 'remove `ai_team.review_gate.managed`',
2091
+ };
2092
+ }
2093
+ return {
2094
+ id: 'team',
2095
+ status: 'ok',
2096
+ message: 'team mode not configured (default-off)',
2097
+ remedy:
2098
+ 'opt in via `ai_team.enabled: true` in .agent-settings.yml, ' +
2099
+ 'then re-run `agent-config doctor --check team`',
2100
+ };
2101
+ }
2102
+
2103
+ const remedies: string[] = [];
2104
+
2105
+ // (a) codex binary — council-probe discovery reused.
2106
+ const binary_name = _team_codex_binary_name(project_root);
2107
+ const binary_resolved = shutilWhich(binary_name);
2108
+ const binary_glyph = binary_resolved !== null ? '✅' : '❌';
2109
+ if (binary_resolved === null) {
2110
+ remedies.push('install the codex CLI: `npm install -g @openai/codex`');
2111
+ }
2112
+
2113
+ // (a, cont.) codex auth — presence probe of $CODEX_HOME/auth.json.
2114
+ const auth_path = path.join(_codex_home(), 'auth.json');
2115
+ const authed = pathExists(auth_path);
2116
+ const auth_glyph = authed ? '✅' : '❌';
2117
+ if (!authed) {
2118
+ remedies.push('run `codex login`');
2119
+ }
2120
+
2121
+ // (b) official codex plugin — Claude-Code hosts only.
2122
+ const is_claude_host =
2123
+ shutilWhich('claude') !== null || pathExists(claude_plugin.claude_config_dir());
2124
+ let plugin_str: string;
2125
+ if (is_claude_host) {
2126
+ const plugin_present = claude_plugin.codex_plugin_installed();
2127
+ plugin_str = `codex plugin ${plugin_present ? '✅' : '❌'}`;
2128
+ if (!plugin_present) {
2129
+ remedies.push(
2130
+ 'install the official codex plugin: ' +
2131
+ claude_plugin.CODEX_PLUGIN_INSTALL_HINT.map((c) => `\`${c}\``).join(' then '),
2132
+ );
2133
+ }
2134
+ } else {
2135
+ plugin_str = 'codex plugin — (not a Claude Code host; fallback path applies)';
2136
+ }
2137
+
2138
+ // (c) Review-Gate consistency.
2139
+ let gate_str: string;
2140
+ if (gate_managed && !bound_present) {
2141
+ gate_str = 'review-gate ⚠️ on, loop bound absent';
2142
+ remedies.push(
2143
+ 'set `ai_team.review_gate.max_consecutive_blocks` (loop bound; ' +
2144
+ 'ships with the Review-Gate governance phase) or set ' +
2145
+ '`ai_team.review_gate.managed: false`',
2146
+ );
2147
+ } else {
2148
+ gate_str = `review-gate ${gate_managed ? 'on' : 'off'}`;
2149
+ }
2150
+
2151
+ const detail =
2152
+ `codex binary ${binary_glyph} (${binary_name}) · ` +
2153
+ `codex auth ${auth_glyph} (${auth_path}) · ${plugin_str} · ${gate_str}`;
2154
+ if (remedies.length > 0) {
2155
+ return {
2156
+ id: 'team',
2157
+ status: 'warn',
2158
+ message: `${remedies.length} team sub-signal(s) failing · ${detail}`,
2159
+ remedy: remedies.join('; '),
2160
+ };
2161
+ }
2162
+ return {
2163
+ id: 'team',
2164
+ status: 'ok',
2165
+ message: `team mode enabled · ${detail}`,
2166
+ remedy: '',
2167
+ };
2168
+ }
2169
+
1885
2170
  function _check_unsupported_combos(manifest: Dict): Dict {
1886
2171
  const global_only = new Set(['droid', 'qoder']);
1887
2172
  const bad: string[] = [];
@@ -2142,6 +2427,7 @@ function _run_checks(
2142
2427
  const runners: Record<string, CheckRunner> = {
2143
2428
  scope: () => _check_scope(project_root),
2144
2429
  'global-binary': () => _check_global_binary(project_root),
2430
+ 'git-identity': () => _check_git_identity(project_root),
2145
2431
  'claude-plugin': _check_claude_plugin,
2146
2432
  'claude-command-wrappers': _check_claude_command_wrappers,
2147
2433
  'surface-state': _check_surface_state,
@@ -2163,6 +2449,7 @@ function _run_checks(
2163
2449
  'humanizer-runtime': () => _check_humanizer_runtime(),
2164
2450
  'tier-usage-readiness': () => _check_tier_usage_readiness(project_root),
2165
2451
  'council-cli': () => _check_council_cli(project_root),
2452
+ team: () => _check_team(project_root),
2166
2453
  'unsupported-combos': () => _check_unsupported_combos(manifest),
2167
2454
  'wizard-state': _check_wizard_state,
2168
2455
  'settings-review-pending': _check_settings_review_pending,
@@ -2219,6 +2506,7 @@ function _run_checks_no_manifest(
2219
2506
  const runners: Record<string, CheckRunner> = {
2220
2507
  scope: () => _check_scope(project_root),
2221
2508
  'global-binary': () => _check_global_binary(project_root),
2509
+ 'git-identity': () => _check_git_identity(project_root),
2222
2510
  'claude-plugin': _check_claude_plugin,
2223
2511
  'claude-command-wrappers': _check_claude_command_wrappers,
2224
2512
  'surface-state': _check_surface_state,
@@ -2234,6 +2522,7 @@ function _run_checks_no_manifest(
2234
2522
  'humanizer-runtime': () => _check_humanizer_runtime(),
2235
2523
  'tier-usage-readiness': () => _check_tier_usage_readiness(project_root),
2236
2524
  'council-cli': () => _check_council_cli(project_root),
2525
+ team: () => _check_team(project_root),
2237
2526
  'unsupported-combos': () => _skipped_manifest_check('unsupported-combos'),
2238
2527
  'wizard-state': _check_wizard_state,
2239
2528
  'settings-review-pending': _check_settings_review_pending,
@@ -2723,6 +3012,7 @@ export {
2723
3012
  _check_mcp_beta_readiness,
2724
3013
  _check_tier_usage_readiness,
2725
3014
  _check_council_cli,
3015
+ _check_team,
2726
3016
  _check_unsupported_combos,
2727
3017
  _check_wizard_state,
2728
3018
  _check_settings_review_pending,
@@ -27,6 +27,22 @@ export const CLAUDE_PLUGIN_INSTALL_HINT: readonly string[] = [
27
27
  `claude plugin install ${CLAUDE_PLUGIN_ID}@${CLAUDE_MARKETPLACE_NAME}`,
28
28
  ];
29
29
 
30
+ /**
31
+ * Official OpenAI codex plugin for Claude Code (team mode's Claude-Code
32
+ * transport). Marketplace name verified against the upstream repo's
33
+ * `.claude-plugin/marketplace.json` (`name: "openai-codex"`, plugin
34
+ * `name: "codex"`) at HEAD 2026-07-12.
35
+ */
36
+ export const CODEX_PLUGIN_ID = 'codex';
37
+ export const CODEX_MARKETPLACE_NAME = 'openai-codex';
38
+ export const CODEX_MARKETPLACE_REPO = 'openai/codex-plugin-cc';
39
+
40
+ /** The one-time install commands surfaced when the codex plugin is missing. */
41
+ export const CODEX_PLUGIN_INSTALL_HINT: readonly string[] = [
42
+ `claude plugin marketplace add ${CODEX_MARKETPLACE_REPO}`,
43
+ `claude plugin install ${CODEX_PLUGIN_ID}@${CODEX_MARKETPLACE_NAME}`,
44
+ ];
45
+
30
46
  /** Claude Code config dir — `CLAUDE_CONFIG_DIR` env override, else `~/.claude`. */
31
47
  export function claude_config_dir(): string {
32
48
  const env = (process.env['CLAUDE_CONFIG_DIR'] ?? '').trim();
@@ -34,11 +50,11 @@ export function claude_config_dir(): string {
34
50
  }
35
51
 
36
52
  /**
37
- * True when the agent-config plugin is recorded in Claude Code's
53
+ * True when a plugin with the given id is recorded in Claude Code's
38
54
  * `plugins/installed_plugins.json` (any marketplace — matched by the
39
- * `agent-config@` id prefix). Unreadable / absent file → false.
55
+ * `<id>@` key prefix). Unreadable / absent file → false. Read-only.
40
56
  */
41
- export function claude_plugin_installed(): boolean {
57
+ function _plugin_id_recorded(plugin_id: string): boolean {
42
58
  const p = path.join(claude_config_dir(), 'plugins', 'installed_plugins.json');
43
59
  let data: unknown;
44
60
  try {
@@ -50,10 +66,29 @@ export function claude_plugin_installed(): boolean {
50
66
  const plugins = (data as { plugins?: unknown }).plugins;
51
67
  if (plugins === null || plugins === undefined || typeof plugins !== 'object') return false;
52
68
  return Object.keys(plugins as Record<string, unknown>).some((k) =>
53
- k.startsWith(`${CLAUDE_PLUGIN_ID}@`),
69
+ k.startsWith(`${plugin_id}@`),
54
70
  );
55
71
  }
56
72
 
73
+ /**
74
+ * True when the agent-config plugin is recorded in Claude Code's
75
+ * `plugins/installed_plugins.json` (any marketplace — matched by the
76
+ * `agent-config@` id prefix). Unreadable / absent file → false.
77
+ */
78
+ export function claude_plugin_installed(): boolean {
79
+ return _plugin_id_recorded(CLAUDE_PLUGIN_ID);
80
+ }
81
+
82
+ /**
83
+ * True when the official OpenAI codex plugin is recorded in Claude Code's
84
+ * `plugins/installed_plugins.json` (any marketplace — matched by the
85
+ * `codex@` id prefix). Unreadable / absent file → false. Read-only:
86
+ * detection never writes under `~/.claude/`.
87
+ */
88
+ export function codex_plugin_installed(): boolean {
89
+ return _plugin_id_recorded(CODEX_PLUGIN_ID);
90
+ }
91
+
57
92
  /**
58
93
  * Version recorded in the installed marketplace snapshot's
59
94
  * `.claude-plugin/marketplace.json` (`metadata.version` — bumped per
@@ -58,6 +58,29 @@ export const TIERS: readonly string[] = ['public', 'vendor', 'proprietary'];
58
58
  */
59
59
  export const DEFAULT_ALLOWED_TIERS: readonly string[] = ['public'];
60
60
 
61
+ /**
62
+ * Sensitivity axis (Phase 1, road-to-feedback-8.11 / successor note to
63
+ * ADR-119) — orthogonal to `TIERS` (origin) and layered on top of the
64
+ * existing tier + redaction gate. `tier` classifies WHERE a card came from;
65
+ * `sensitivity` classifies WHETHER it may ever leave the project at all.
66
+ *
67
+ * * `prohibited` — contains redaction-class content; never leaves the
68
+ * repo. Machine-derived from the redaction scan (a card is never
69
+ * hand-labelled into this class — a positive hit forces it).
70
+ * * `project` — DEFAULT. Stays project-local; promotion is refused
71
+ * unless a human explicitly reclassifies the card to `shareable`.
72
+ * * `shareable` — eligible for the global store; must pass redaction
73
+ * AND carry full promotion provenance (see the G5 footer fields).
74
+ *
75
+ * Deliberately kept to three machine-anchored classes — a wider
76
+ * team/organization taxonomy is phantom for a single-maintainer install and
77
+ * would over-fit; revisit only once a real multi-tenant consumer exists.
78
+ */
79
+ export const SENSITIVITIES: readonly string[] = ['prohibited', 'project', 'shareable'];
80
+
81
+ /** Default sensitivity for a card with no explicit `sensitivity:` field. */
82
+ export const DEFAULT_SENSITIVITY = 'project';
83
+
61
84
  /**
62
85
  * Hard defaults applied when the setting block is absent. Default is ON for
63
86
  * the safe tiers; turning sharing on never makes promotion automatic.
@@ -384,6 +407,14 @@ const _PROVENANCE_FIELDS: readonly string[] = [
384
407
  'last_verified',
385
408
  'tier',
386
409
  'seen_in',
410
+ // G5 promotion audit-trail fields (Phase 1, road-to-feedback-8.11):
411
+ // the more consistent home for these than frontmatter, since they sit
412
+ // alongside the other promotion-time facts above, not the card's own
413
+ // origin-tier/sensitivity classification (which lives in frontmatter).
414
+ 'source_repo',
415
+ 'owner',
416
+ 'review_after',
417
+ 'promotion_reason',
387
418
  ];
388
419
 
389
420
  /** Render the global-card provenance footer (the unversioned-store audit trail). */
@@ -394,6 +425,10 @@ export function render_provenance_footer(args: {
394
425
  last_verified: string;
395
426
  tier: string;
396
427
  seen_in: string[];
428
+ source_repo: string;
429
+ owner: string;
430
+ review_after: string;
431
+ promotion_reason: string;
397
432
  }): string {
398
433
  return [
399
434
  PROVENANCE_START,
@@ -403,11 +438,20 @@ export function render_provenance_footer(args: {
403
438
  `- last_verified: ${args.last_verified}`,
404
439
  `- tier: ${args.tier}`,
405
440
  `- seen_in: ${args.seen_in.join(', ')}`,
441
+ `- source_repo: ${args.source_repo}`,
442
+ `- owner: ${args.owner}`,
443
+ `- review_after: ${args.review_after}`,
444
+ `- promotion_reason: ${_singleLine(args.promotion_reason)}`,
406
445
  PROVENANCE_END,
407
446
  '',
408
447
  ].join('\n');
409
448
  }
410
449
 
450
+ /** Collapse a human-entered reason to one line — footer fields are line-based. */
451
+ function _singleLine(s: string): string {
452
+ return s.replace(/\r\n|\r|\n/g, ' ').trim();
453
+ }
454
+
411
455
  /** Extract the provenance footer fields as a flat dict (empty if absent). */
412
456
  export function parse_provenance_footer(text: string): Record<string, string> {
413
457
  if (!text.includes(PROVENANCE_START) || !text.includes(PROVENANCE_END)) {
@@ -187,6 +187,156 @@ export function should_suggest(
187
187
  return len >= options.threshold;
188
188
  }
189
189
 
190
+ // ---------------------------------------------------------------------------
191
+ // Sensitivity gate (Phase 1, road-to-feedback-8.11 — successor note to
192
+ // ADR-119). Layered ON TOP of the existing tier/redaction gate in
193
+ // `knowledge_global_redaction.ts`: a card must clear tier + redaction FIRST,
194
+ // then clear this gate before its write is allowed to proceed.
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /** Outcome of the sensitivity gate for one promotion attempt. */
198
+ export interface SensitivityGateResult {
199
+ readonly eligible: boolean;
200
+ readonly reason: string;
201
+ /** The EFFECTIVE sensitivity used for the decision (post auto-derivation). */
202
+ readonly sensitivity: string;
203
+ }
204
+
205
+ /**
206
+ * Resolve the effective sensitivity used for a promotion decision.
207
+ *
208
+ * A redaction hit always forces `prohibited`, overriding any declared value —
209
+ * this is independent of the tier gate's own `halt_on_trigger` setting, so a
210
+ * `prohibited` override never depends on that unrelated config flag. An
211
+ * unset / invalid declared value defaults to `project` (never `shareable` —
212
+ * `shareable` is never auto-assigned, only explicitly declared by a human).
213
+ */
214
+ export function resolve_effective_sensitivity(declared: string, violations_present: boolean): string {
215
+ if (violations_present) {
216
+ return 'prohibited';
217
+ }
218
+ const d = (declared ?? '').trim();
219
+ return knowledge_global.SENSITIVITIES.includes(d) ? d : knowledge_global.DEFAULT_SENSITIVITY;
220
+ }
221
+
222
+ /**
223
+ * Gate a card for promotion on the sensitivity axis.
224
+ *
225
+ * Refuses anything whose effective sensitivity is not `shareable` (including
226
+ * the machine-derived `prohibited` override), and refuses a `shareable` card
227
+ * that has no human-entered `promotion_reason` — the auto-promote
228
+ * *suggestion* only ever leads to a write once a human states why the card
229
+ * is safe to share.
230
+ */
231
+ export function gate_sensitivity_for_promotion(
232
+ declared_sensitivity: string,
233
+ options: { violations_present?: boolean; promotion_reason?: string } = {},
234
+ ): SensitivityGateResult {
235
+ const violations_present = options.violations_present ?? false;
236
+ const effective = resolve_effective_sensitivity(declared_sensitivity, violations_present);
237
+
238
+ if (effective === 'prohibited') {
239
+ return {
240
+ eligible: false,
241
+ sensitivity: effective,
242
+ reason:
243
+ 'sensitivity: prohibited — redaction-class content, never leaves the repo',
244
+ };
245
+ }
246
+ if (effective !== 'shareable') {
247
+ return {
248
+ eligible: false,
249
+ sensitivity: effective,
250
+ reason:
251
+ `sensitivity '${effective}' — promotion refused unless a human explicitly ` +
252
+ "reclassifies the card to 'shareable'",
253
+ };
254
+ }
255
+ const reason = (options.promotion_reason ?? '').trim();
256
+ if (!reason) {
257
+ return {
258
+ eligible: false,
259
+ sensitivity: effective,
260
+ reason:
261
+ "sensitivity 'shareable' but no promotion_reason given — a human must state " +
262
+ 'why this card is safe to share before it may be promoted',
263
+ };
264
+ }
265
+ return { eligible: true, sensitivity: effective, reason: 'sensitivity gate passed' };
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Revocation ledger — append-only tombstone trail (Phase 1, road-to-feedback-8.11)
270
+ // ---------------------------------------------------------------------------
271
+
272
+ export const REVOCATIONS_FILENAME = '.revocations.jsonl';
273
+
274
+ /** One append-only tombstone line: what was revoked, when, and why. */
275
+ export interface RevocationEntry {
276
+ readonly revoked_at: string;
277
+ readonly card_id: string;
278
+ readonly reason: string;
279
+ }
280
+
281
+ function _revocations_path(env: knowledge_global.EnvMapType): string {
282
+ return path.join(knowledge_global.global_store_dir(env, { create: true }), REVOCATIONS_FILENAME);
283
+ }
284
+
285
+ /**
286
+ * Append one tombstone line to the revocation ledger — the caller MUST call
287
+ * this BEFORE deleting the card/usage entry it documents. Append-only (a
288
+ * single `fs.appendFileSync`, never rewritten): no `forget` / `forget --tier`
289
+ * / `purge` call ever removes a prior tombstone, so the ledger stays a
290
+ * durable audit trail across every deletion path — including `purge`, which
291
+ * deliberately spares this one file while wiping everything else.
292
+ */
293
+ export function append_tombstone(
294
+ card_id: string,
295
+ reason: string,
296
+ options: { today?: string; env?: knowledge_global.EnvMapType } = {},
297
+ ): RevocationEntry {
298
+ const entry: RevocationEntry = {
299
+ revoked_at: options.today ?? '',
300
+ card_id,
301
+ reason: reason || 'no reason given',
302
+ };
303
+ fs.appendFileSync(_revocations_path(options.env ?? null), JSON.stringify(entry) + '\n', 'utf-8');
304
+ return entry;
305
+ }
306
+
307
+ /** Read the revocation ledger. Tolerant: missing file → []; malformed lines are skipped. */
308
+ export function load_tombstones(env: knowledge_global.EnvMapType = null): RevocationEntry[] {
309
+ let text: string;
310
+ try {
311
+ text = fs.readFileSync(
312
+ path.join(knowledge_global.global_store_dir(env), REVOCATIONS_FILENAME),
313
+ 'utf-8',
314
+ );
315
+ } catch {
316
+ return [];
317
+ }
318
+ const out: RevocationEntry[] = [];
319
+ for (const raw of text.split(/\r\n|\r|\n/)) {
320
+ const line = raw.trim();
321
+ if (!line) {
322
+ continue;
323
+ }
324
+ try {
325
+ const obj: unknown = JSON.parse(line);
326
+ if (_isPlainObject(obj) && typeof obj['card_id'] === 'string') {
327
+ out.push({
328
+ revoked_at: String(obj['revoked_at'] ?? ''),
329
+ card_id: obj['card_id'],
330
+ reason: String(obj['reason'] ?? ''),
331
+ });
332
+ }
333
+ } catch {
334
+ // corrupt line — skip; never let one bad line crash the ledger read
335
+ }
336
+ }
337
+ return out;
338
+ }
339
+
190
340
  /**
191
341
  * List cards that warrant a promotion suggestion under the current config.
192
342
  *