@guilz-dev/belay 0.2.0 → 0.3.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 (76) hide show
  1. package/README.md +39 -12
  2. package/dist/adapters/shared/gate-runtime.js +13 -5
  3. package/dist/bundle/claude-runtime.mjs +1659 -371
  4. package/dist/bundle/codex-runtime.mjs +1658 -371
  5. package/dist/bundle/cursor-runtime.mjs +1659 -371
  6. package/dist/cli.js +198 -8
  7. package/dist/commands/approve.js +11 -0
  8. package/dist/commands/config.d.ts +70 -0
  9. package/dist/commands/config.js +494 -0
  10. package/dist/commands/doctor.js +2 -1
  11. package/dist/commands/health-snapshot.d.ts +6 -0
  12. package/dist/commands/health-snapshot.js +34 -0
  13. package/dist/commands/judge.d.ts +90 -0
  14. package/dist/commands/judge.js +285 -0
  15. package/dist/commands/status.js +2 -2
  16. package/dist/commands/stdin-key.d.ts +1 -0
  17. package/dist/commands/stdin-key.js +8 -0
  18. package/dist/conformance/guarantee-table.js +12 -0
  19. package/dist/conformance/types.d.ts +2 -0
  20. package/dist/core/audit-io.d.ts +2 -0
  21. package/dist/core/audit-io.js +14 -0
  22. package/dist/core/capability/index.d.ts +2 -2
  23. package/dist/core/capability/index.js +2 -2
  24. package/dist/core/capability/paths.d.ts +1 -0
  25. package/dist/core/capability/paths.js +74 -6
  26. package/dist/core/capability/reasons.d.ts +3 -0
  27. package/dist/core/capability/reasons.js +8 -0
  28. package/dist/core/classify-tool.js +97 -27
  29. package/dist/core/config-layers.js +2 -1
  30. package/dist/core/config.d.ts +22 -2
  31. package/dist/core/config.js +110 -11
  32. package/dist/core/credential-store.d.ts +11 -0
  33. package/dist/core/credential-store.js +60 -0
  34. package/dist/core/gate-engine.js +104 -13
  35. package/dist/core/integrity.d.ts +2 -0
  36. package/dist/core/integrity.js +13 -0
  37. package/dist/core/judge-api-key.d.ts +19 -0
  38. package/dist/core/judge-api-key.js +74 -0
  39. package/dist/core/judge-cloud-consent.d.ts +13 -0
  40. package/dist/core/judge-cloud-consent.js +38 -0
  41. package/dist/core/judge-config.d.ts +41 -4
  42. package/dist/core/judge-config.js +263 -57
  43. package/dist/core/judge-doctor.d.ts +6 -1
  44. package/dist/core/judge-doctor.js +147 -96
  45. package/dist/core/judge-model-discovery.d.ts +24 -0
  46. package/dist/core/judge-model-discovery.js +168 -0
  47. package/dist/core/judge-model-policy.d.ts +5 -0
  48. package/dist/core/judge-model-policy.js +21 -0
  49. package/dist/core/judge-runtime-detection.d.ts +9 -0
  50. package/dist/core/judge-runtime-detection.js +68 -0
  51. package/dist/core/transactional/diff-evaluator.js +1 -19
  52. package/dist/core/types.d.ts +2 -0
  53. package/dist/core/verdict/adapter.d.ts +1 -0
  54. package/dist/core/verdict/adapter.js +7 -1
  55. package/dist/core/verdict/containment.d.ts +5 -0
  56. package/dist/core/verdict/containment.js +32 -2
  57. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  58. package/dist/core/verdict/judge-catalog.js +148 -0
  59. package/dist/core/verdict/judge-cli.d.ts +23 -0
  60. package/dist/core/verdict/judge-cli.js +280 -0
  61. package/dist/core/verdict/judge-factory.d.ts +15 -4
  62. package/dist/core/verdict/judge-factory.js +117 -14
  63. package/dist/core/verdict/judge.d.ts +20 -1
  64. package/dist/core/verdict/judge.js +83 -14
  65. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  66. package/dist/core/verdict/persistent-paths.js +52 -0
  67. package/dist/core/verdict/types.d.ts +6 -2
  68. package/dist/core/verdict/verdict.js +161 -47
  69. package/dist/installer.js +66 -15
  70. package/dist/types.d.ts +7 -0
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/package.json +1 -1
  74. package/skills/belay/SKILL.md +8 -7
  75. package/dist/commands/init-wizard.d.ts +0 -21
  76. package/dist/commands/init-wizard.js +0 -63
@@ -1,7 +1,14 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { repoLocalStateDirFor } from '../../config-io.js';
3
4
  import { normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
5
+ import { resolveJudgeCredential } from '../judge-api-key.js';
6
+ import { hasValidCloudConsent, isCloudJudgeConfig } from '../judge-config.js';
7
+ import { rejectDeprecatedJudgeModelAuto } from '../judge-model-policy.js';
8
+ import { detectJudgeRuntimeCapabilities } from '../judge-runtime-detection.js';
4
9
  import { createDeterministicJudgeStub, createFailClosedJudge, createOllamaJudge, createOpenAiCompatibleJudge, } from './judge.js';
10
+ import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, } from './judge-catalog.js';
11
+ import { createClaudeCliJudge, createCodexCliJudge, createCursorCliJudge } from './judge-cli.js';
5
12
  const FIXTURE_MODELS_URL = new URL('../../../fixtures/judge-models.json', import.meta.url);
6
13
  let cachedPinnedModels = null;
7
14
  export function resetPinnedJudgeModelsCache() {
@@ -24,33 +31,99 @@ export async function loadPinnedJudgeModels() {
24
31
  return cachedPinnedModels;
25
32
  }
26
33
  }
27
- export function resolveCloudModel(requested, pinned) {
28
- if (requested === 'auto') {
29
- const envResolved = process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim();
30
- return {
31
- requested,
32
- resolved: envResolved || pinned.autoResolved,
33
- };
34
+ export function resolveJudgeModel(judge) {
35
+ rejectDeprecatedJudgeModelAuto(judge.model);
36
+ const requested = judge.model;
37
+ const envOverride = testJudgeModelOverride();
38
+ return { requested, resolved: envOverride || requested };
39
+ }
40
+ function testJudgeModelOverride() {
41
+ if (!process.env.VITEST && !process.env.VITEST_WORKER_ID) {
42
+ return undefined;
34
43
  }
35
- return { requested, resolved: requested };
44
+ return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || undefined;
36
45
  }
37
- /** @deprecated Use resolveCloudModel */
46
+ /** @deprecated Use resolveJudgeModel */
47
+ export const resolveCloudModel = (requested, _pinned) => {
48
+ rejectDeprecatedJudgeModelAuto(requested);
49
+ const envResolved = testJudgeModelOverride();
50
+ return {
51
+ requested,
52
+ resolved: envResolved || requested,
53
+ };
54
+ };
55
+ /** @deprecated Use resolveJudgeModel */
38
56
  export const resolveCursorModel = resolveCloudModel;
57
+ function providerIdFromJudge(judge) {
58
+ if (judge.providerId) {
59
+ const normalized = normalizeLegacyProviderId(judge.providerId);
60
+ if (normalized) {
61
+ return normalized;
62
+ }
63
+ }
64
+ return inferProviderIdFromConfig(judge);
65
+ }
66
+ function createCliJudgeForProvider(providerId, judgeConfig, config) {
67
+ const { resolved } = resolveJudgeModel(judgeConfig);
68
+ const options = {
69
+ modelRequested: judgeConfig.model,
70
+ modelResolved: resolved,
71
+ timeoutMs: judgeConfig.timeoutMs,
72
+ sensitivePaths: config.classifier.sensitivePaths,
73
+ scrubOptions: scrubOptionsFromConfig(config),
74
+ };
75
+ if (providerId === 'codex') {
76
+ return createCodexCliJudge(options);
77
+ }
78
+ if (providerId === 'cursor') {
79
+ return createCursorCliJudge(options);
80
+ }
81
+ return createClaudeCliJudge(options);
82
+ }
39
83
  export function createJudgeFromConfig(config, options = {}) {
84
+ if (process.env.BELAY_DETERMINISTIC_JUDGE === '1') {
85
+ return createDeterministicJudgeStub();
86
+ }
40
87
  const judgeConfig = config.judge;
88
+ if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
89
+ const { resolved } = resolveJudgeModel(judgeConfig);
90
+ return createFailClosedJudge({
91
+ reason: 'judge_provider_removed',
92
+ fallbackReason: 'provider_migration_required',
93
+ modelRequested: judgeConfig.model,
94
+ modelResolved: resolved,
95
+ });
96
+ }
41
97
  const provider = normalizeJudgeProvider(judgeConfig.provider);
98
+ const providerId = providerIdFromJudge(judgeConfig);
99
+ const catalogSpec = getJudgeProviderSpec(providerId);
100
+ const { resolved } = resolveJudgeModel(judgeConfig);
101
+ const runtime = detectJudgeRuntimeCapabilities(providerId);
42
102
  if (provider === 'openai-compatible') {
43
103
  const endpoint = judgeConfig.endpoint?.trim();
104
+ if (!endpoint && runtime.cliTransport) {
105
+ if (providerId === 'codex' || providerId === 'cursor') {
106
+ return createCliJudgeForProvider(providerId, judgeConfig, config);
107
+ }
108
+ }
44
109
  if (!endpoint) {
45
110
  return createFailClosedJudge({
46
111
  reason: 'openai_compatible_endpoint_missing',
47
112
  fallbackReason: 'missing_endpoint',
48
113
  modelRequested: judgeConfig.model,
49
- modelResolved: judgeConfig.model,
114
+ modelResolved: resolved,
115
+ });
116
+ }
117
+ if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
118
+ return createFailClosedJudge({
119
+ reason: 'openai_compatible_consent_missing',
120
+ fallbackReason: 'cloud_consent_missing',
121
+ modelRequested: judgeConfig.model,
122
+ modelResolved: resolved,
50
123
  });
51
124
  }
52
- const pinned = options.pinnedModels ?? { autoResolved: 'composer-2.5' };
53
- const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
125
+ const repoRoot = options.repoRoot ?? process.cwd();
126
+ const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
54
127
  return createOpenAiCompatibleJudge({
55
128
  endpoint,
56
129
  modelRequested: judgeConfig.model,
@@ -58,6 +131,13 @@ export function createJudgeFromConfig(config, options = {}) {
58
131
  timeoutMs: judgeConfig.timeoutMs,
59
132
  sensitivePaths: config.classifier.sensitivePaths,
60
133
  scrubOptions: scrubOptionsFromConfig(config),
134
+ resolveApiKey: async () => resolveJudgeCredential({
135
+ judge: judgeConfig,
136
+ catalogSpec: catalogSpec ?? undefined,
137
+ repoRoot,
138
+ repoLocalStateDir,
139
+ config,
140
+ }),
61
141
  });
62
142
  }
63
143
  if (provider === 'ollama') {
@@ -68,8 +148,31 @@ export function createJudgeFromConfig(config, options = {}) {
68
148
  keepAlive: judgeConfig.keepAlive,
69
149
  });
70
150
  }
71
- return createDeterministicJudgeStub();
151
+ if (provider === 'anthropic') {
152
+ if (runtime.cliTransport) {
153
+ return createClaudeCliJudge({
154
+ modelRequested: judgeConfig.model,
155
+ modelResolved: resolved,
156
+ timeoutMs: judgeConfig.timeoutMs,
157
+ sensitivePaths: config.classifier.sensitivePaths,
158
+ scrubOptions: scrubOptionsFromConfig(config),
159
+ });
160
+ }
161
+ return createFailClosedJudge({
162
+ reason: 'anthropic_not_implemented',
163
+ fallbackReason: 'anthropic_runtime_unavailable',
164
+ modelRequested: judgeConfig.model,
165
+ modelResolved: resolved,
166
+ });
167
+ }
168
+ return createFailClosedJudge({
169
+ reason: 'unsupported_judge_provider',
170
+ fallbackReason: 'unsupported_provider',
171
+ modelRequested: judgeConfig.model,
172
+ modelResolved: resolved,
173
+ });
72
174
  }
73
175
  export function judgeConfigSummary(judge) {
74
- return `${judge.provider}/${judge.model}`;
176
+ const id = judge.providerId ?? judge.provider;
177
+ return `${id}/${judge.model}`;
75
178
  }
@@ -1,17 +1,32 @@
1
1
  import type { ScrubOptions } from '../types.js';
2
2
  import type { Tier1Judge, Tier1Verdict } from './types.js';
3
+ export type Tier1JudgeTransport = 'http' | 'ollama-http' | 'codex-cli' | 'cursor-cli' | 'claude-cli' | 'unavailable';
3
4
  export interface Tier1JudgeTrace {
4
- provider: 'openai-compatible' | 'ollama' | 'fallback';
5
+ provider: 'openai-compatible' | 'ollama' | 'anthropic' | 'fallback';
5
6
  modelRequested: string;
6
7
  modelResolved: string;
7
8
  latencyMs: number;
9
+ transport?: Tier1JudgeTransport;
8
10
  outboundRedacted?: boolean;
9
11
  fallbackReason?: string;
10
12
  }
11
13
  export interface TracedTier1Judge extends Tier1Judge {
12
14
  lastTrace?: Tier1JudgeTrace;
13
15
  }
16
+ export declare function buildTier1Prompt(text: string): string;
17
+ export declare function parseTier1Json(raw: string): Tier1Verdict | null;
14
18
  export declare function prescanInterpreterCode(code: string): Tier1Verdict | null;
19
+ export interface MutationPrescanParams {
20
+ targets: string[];
21
+ cwd: string;
22
+ repoRoot: string;
23
+ trustedCwd: boolean;
24
+ sensitivePaths: string[];
25
+ }
26
+ /** ADR-002 M3: structural prescan for sensitive / persistent mutation targets (shell redirects, etc.). */
27
+ export declare function prescanMutationTargets(params: MutationPrescanParams): Tier1Verdict | null;
28
+ /** Returns prescan verdict when structural M3 rules require ask (before Tier1 LLM). */
29
+ export declare function mutationPrescanRequiresAsk(params: MutationPrescanParams): Tier1Verdict | null;
15
30
  /** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
16
31
  export declare function createDeterministicJudgeStub(): TracedTier1Judge;
17
32
  /** Fail-closed judge for when Tier1 is required but unavailable. */
@@ -35,6 +50,10 @@ export interface OpenAiCompatibleJudgeOptions {
35
50
  modelResolved: string;
36
51
  timeoutMs: number;
37
52
  apiKey?: string;
53
+ resolveApiKey?: () => Promise<{
54
+ key: string | null;
55
+ source: string | null;
56
+ }>;
38
57
  sensitivePaths: string[];
39
58
  scrubOptions: ScrubOptions;
40
59
  fetchImpl?: typeof fetch;
@@ -1,34 +1,60 @@
1
+ import { matchesSensitivePath } from '../glob.js';
2
+ import { relativeWithinRepo, resolveMutationTarget } from '../path-utils.js';
3
+ import { resolveTrustedPath } from './containment.js';
1
4
  import { scrubOutboundForJudge } from './judge-outbound.js';
5
+ import { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
2
6
  const SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
3
7
  const DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
4
8
  const GIT_PATTERNS = /\.git\b/i;
5
- const TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"reason":string}
9
+ const TIER1_PROMPT = `Answer ONLY with JSON: {"local_recoverable":boolean,"destroys_history_or_secrets":boolean,"reason":string}
6
10
  reason should be a short snake_case label.
7
11
 
8
- external_change: true if this operation CREATEs, MODIFYs, DELETEs, or SENDs data to any system outside the local machine and its git repository working tree.
9
- READ/QUERY/GET/list/describe/SELECT/HTTP GET is NOT a change. Local build, test, and package install inside the repo are NOT external changes.
10
- If unsure, answer true.
12
+ local_recoverable: true if a mistaken operation can be undone locally (git revert, fs snapshot, trivial manual revert).
13
+ local_recoverable: false if remote/destructive (git push --force, aws s3 rm, secret send) OR persistent local harm (~/.ssh/authorized_keys, shell rc, crontab, ~/.config persistence).
14
+
15
+ destroys_history_or_secrets: true if the operation deletes or corrupts git history (.git), overwrites secrets/credentials, or modifies persistent agent startup (shell rc, crontab, launchd, authorized_keys).
16
+
17
+ Examples local_recoverable=true, destroys_history_or_secrets=false: IDE plan files under the user home, /tmp files, in-repo edits, READ/QUERY/GET.
18
+ Examples local_recoverable=false OR destroys_history_or_secrets=true: remote mutations, credential paths, agent startup persistence, rm -rf .git.
19
+ If genuinely ambiguous about recoverability or persistent harm, answer false.
11
20
 
12
21
  Command/code:
13
22
  `;
14
23
  function failClosedVerdict(reason) {
15
24
  return {
16
- external_change: true,
25
+ local_recoverable: false,
17
26
  destroys_outside_repo: true,
18
27
  destroys_history_or_secrets: true,
28
+ external_change: true,
19
29
  reason,
20
30
  };
21
31
  }
22
- function parseTier1Json(raw) {
32
+ function parseLocalRecoverable(parsed) {
33
+ if (typeof parsed.local_recoverable === 'boolean') {
34
+ return parsed.local_recoverable;
35
+ }
36
+ if (typeof parsed.external_change === 'boolean') {
37
+ return !parsed.external_change;
38
+ }
39
+ return null;
40
+ }
41
+ export function buildTier1Prompt(text) {
42
+ return `${TIER1_PROMPT}${text}`;
43
+ }
44
+ export function parseTier1Json(raw) {
23
45
  try {
24
46
  const parsed = JSON.parse(raw);
25
- if (typeof parsed.external_change !== 'boolean') {
47
+ const localRecoverable = parseLocalRecoverable(parsed);
48
+ if (localRecoverable === null) {
26
49
  return null;
27
50
  }
28
51
  return {
52
+ local_recoverable: localRecoverable,
53
+ destroys_outside_repo: typeof parsed.destroys_outside_repo === 'boolean' ? parsed.destroys_outside_repo : false,
54
+ destroys_history_or_secrets: typeof parsed.destroys_history_or_secrets === 'boolean'
55
+ ? parsed.destroys_history_or_secrets
56
+ : false,
29
57
  external_change: parsed.external_change,
30
- destroys_outside_repo: false,
31
- destroys_history_or_secrets: false,
32
58
  reason: typeof parsed.reason === 'string' ? parsed.reason : 'tier1_llm',
33
59
  };
34
60
  }
@@ -43,7 +69,7 @@ export function prescanInterpreterCode(code) {
43
69
  const hitsDestructive = DESTRUCTIVE_VERBS.test(normalized);
44
70
  if ((hitsSecret || hitsGit) && hitsDestructive) {
45
71
  return {
46
- external_change: false,
72
+ local_recoverable: true,
47
73
  destroys_outside_repo: false,
48
74
  destroys_history_or_secrets: true,
49
75
  reason: 'prescan_destructive_secret',
@@ -51,12 +77,54 @@ export function prescanInterpreterCode(code) {
51
77
  }
52
78
  return null;
53
79
  }
80
+ /** ADR-002 M3: structural prescan for sensitive / persistent mutation targets (shell redirects, etc.). */
81
+ export function prescanMutationTargets(params) {
82
+ for (const target of params.targets) {
83
+ const resolved = resolveTrustedPath(target, params.cwd, params.trustedCwd) ??
84
+ resolveMutationTarget(target, params.cwd);
85
+ if (!resolved) {
86
+ continue;
87
+ }
88
+ const relative = relativeWithinRepo(params.repoRoot, resolved);
89
+ if (relative !== null &&
90
+ matchesSensitivePath(relative.replaceAll('\\', '/'), params.sensitivePaths)) {
91
+ return {
92
+ local_recoverable: false,
93
+ destroys_outside_repo: false,
94
+ destroys_history_or_secrets: true,
95
+ reason: 'sensitive_path_mutation',
96
+ };
97
+ }
98
+ if (relative === null && isOutsideRepoSecretCredentialPath(resolved)) {
99
+ return {
100
+ local_recoverable: false,
101
+ destroys_outside_repo: false,
102
+ destroys_history_or_secrets: true,
103
+ reason: 'outside_repo_secret_credential_path',
104
+ };
105
+ }
106
+ if (isPersistentAgentPath(resolved)) {
107
+ return {
108
+ local_recoverable: false,
109
+ destroys_outside_repo: false,
110
+ destroys_history_or_secrets: true,
111
+ reason: 'persistent_agent_path',
112
+ };
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ /** Returns prescan verdict when structural M3 rules require ask (before Tier1 LLM). */
118
+ export function mutationPrescanRequiresAsk(params) {
119
+ const prescan = prescanMutationTargets(params);
120
+ return prescan && tier1RequiresAsk(prescan) ? prescan : null;
121
+ }
54
122
  /** Conservative stub: Tier1 defers to Tier0; returns safe negatives for structural suite. */
55
123
  export function createDeterministicJudgeStub() {
56
124
  return {
57
125
  evaluate() {
58
126
  return Promise.resolve({
59
- external_change: false,
127
+ local_recoverable: true,
60
128
  destroys_outside_repo: false,
61
129
  destroys_history_or_secrets: false,
62
130
  reason: 'deterministic_stub',
@@ -190,8 +258,9 @@ export function createOpenAiCompatibleJudge(options) {
190
258
  };
191
259
  return failClosedVerdict('outbound_scrub_failed');
192
260
  }
193
- const { resolveJudgeApiKey } = await import('../judge-api-key.js');
194
- const resolvedKey = resolveJudgeApiKey();
261
+ const resolvedKey = options.resolveApiKey
262
+ ? await options.resolveApiKey()
263
+ : { key: options.apiKey ?? null, source: null };
195
264
  const apiKey = options.apiKey ?? resolvedKey.key;
196
265
  if (!apiKey) {
197
266
  judge.lastTrace = {
@@ -258,5 +327,5 @@ export function createOpenAiCompatibleJudge(options) {
258
327
  /** @deprecated Use createOpenAiCompatibleJudge */
259
328
  export const createCursorJudge = createOpenAiCompatibleJudge;
260
329
  export function tier1RequiresAsk(verdict) {
261
- return verdict.external_change || verdict.destroys_history_or_secrets;
330
+ return !verdict.local_recoverable || verdict.destroys_history_or_secrets;
262
331
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * M3 catalog: paths that survive reboot or change agent startup.
3
+ * @see docs/adr/tier0-retention-ledger.md
4
+ */
5
+ export declare const PERSISTENT_AGENT_PATH_MARKERS: readonly [".ssh/authorized_keys", ".zshrc", ".bashrc", ".bash_profile", ".profile", ".config/", "/crontab", "LaunchAgents/", "/Library/LaunchDaemons/"];
6
+ export declare const OUTSIDE_REPO_SECRET_CREDENTIAL_MARKERS: readonly [".env", ".env.*", "*.pem", ".ssh/id_*", ".aws/", ".npmrc", ".git-credentials", ".netrc", ".kube/config", ".docker/config.json", ".gnupg/", ".pypirc"];
7
+ export declare function isPersistentAgentPath(resolvedPath: string): boolean;
8
+ export declare function isOutsideRepoSecretCredentialPath(resolvedPath: string): boolean;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * M3 catalog: paths that survive reboot or change agent startup.
3
+ * @see docs/adr/tier0-retention-ledger.md
4
+ */
5
+ export const PERSISTENT_AGENT_PATH_MARKERS = [
6
+ '.ssh/authorized_keys',
7
+ '.zshrc',
8
+ '.bashrc',
9
+ '.bash_profile',
10
+ '.profile',
11
+ '.config/',
12
+ '/crontab',
13
+ 'LaunchAgents/',
14
+ '/Library/LaunchDaemons/',
15
+ ];
16
+ export const OUTSIDE_REPO_SECRET_CREDENTIAL_MARKERS = [
17
+ '.env',
18
+ '.env.*',
19
+ '*.pem',
20
+ '.ssh/id_*',
21
+ '.aws/',
22
+ '.npmrc',
23
+ '.git-credentials',
24
+ '.netrc',
25
+ '.kube/config',
26
+ '.docker/config.json',
27
+ '.gnupg/',
28
+ '.pypirc',
29
+ ];
30
+ export function isPersistentAgentPath(resolvedPath) {
31
+ const normalized = resolvedPath.replaceAll('\\', '/');
32
+ return (/\.ssh\/authorized_keys$/i.test(normalized) ||
33
+ /\/\.(zshrc|bashrc|bash_profile|profile)$/i.test(normalized) ||
34
+ /\/\.config\//i.test(normalized) ||
35
+ /\/crontab$/i.test(normalized) ||
36
+ /LaunchAgents\//i.test(normalized) ||
37
+ /\/Library\/LaunchDaemons\//i.test(normalized));
38
+ }
39
+ export function isOutsideRepoSecretCredentialPath(resolvedPath) {
40
+ const normalized = resolvedPath.replaceAll('\\', '/');
41
+ return (/\/\.env(?:\.[^/]+)?$/i.test(normalized) ||
42
+ /\/[^/]+\.pem$/i.test(normalized) ||
43
+ /\/\.ssh\/id_[^/]+$/i.test(normalized) ||
44
+ /\/\.aws\//i.test(normalized) ||
45
+ /\/\.npmrc$/i.test(normalized) ||
46
+ /\/\.git-credentials$/i.test(normalized) ||
47
+ /\/\.netrc$/i.test(normalized) ||
48
+ /\/\.kube\/config$/i.test(normalized) ||
49
+ /\/\.docker\/config\.json$/i.test(normalized) ||
50
+ /\/\.gnupg\//i.test(normalized) ||
51
+ /\/\.pypirc$/i.test(normalized));
52
+ }
@@ -16,10 +16,14 @@ export interface VerdictResult {
16
16
  judgeTrace?: JudgeTrace;
17
17
  }
18
18
  export interface Tier1Verdict {
19
- external_change: boolean;
20
- destroys_outside_repo: boolean;
19
+ /** true = undoable via git/fs snapshot or trivial local revert (ADR-002 local-recoverable). */
20
+ local_recoverable: boolean;
21
21
  destroys_history_or_secrets: boolean;
22
22
  reason: string;
23
+ /** @deprecated Legacy LLM field; parsed only for migration. */
24
+ external_change?: boolean;
25
+ /** @deprecated Unused in decision logic. */
26
+ destroys_outside_repo?: boolean;
23
27
  }
24
28
  export interface Tier1EvaluateInput {
25
29
  text: string;