@codapult/guard 0.3.0 → 0.5.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.
package/README.md CHANGED
@@ -231,12 +231,37 @@ Guard state is stored in `.codapult/guard/`:
231
231
  ├── proposals.json evidence and approval history
232
232
  ├── baseline.json accepted pre-existing findings
233
233
  ├── agent.json AI-host completion-gate configuration
234
- └── history/ project snapshots for comparison
234
+ └── history/ project snapshots and local verification run manifests
235
235
  ```
236
236
 
237
237
  Commit policy and baseline files when the team wants shared guardrails. Treat cache artifacts as
238
238
  disposable according to the project’s policy, and never commit secrets.
239
239
 
240
+ ### Optional scoped budgets
241
+
242
+ Budgets are policy, not a universal style rule. Add them only for a named risk boundary such as a
243
+ service, route handler, or dependency-heavy module:
244
+
245
+ ```json
246
+ {
247
+ "id": "service-lines",
248
+ "description": "Services must remain reviewable.",
249
+ "metric": "lines",
250
+ "scope": ["src/services"],
251
+ "limit": 300,
252
+ "severity": "warning",
253
+ "reason": "Keep service changes reviewable by one owner.",
254
+ "status": "active"
255
+ }
256
+ ```
257
+
258
+ Place budgets in the `budgets` array in `.codapult/guard/rules.json`. Supported metrics are
259
+ `lines`, `bytes`, and `imports`. Generated files, schemas, migrations, and other paths are not
260
+ checked unless they are explicitly included in `scope`; a wildcard such as `*` can intentionally
261
+ cover the project root. Invalid, absolute, parent-directory, or nonexistent scopes fail the Guard
262
+ gate. Changing a budget is a policy decision:
263
+ review the diff and update its written `reason` rather than silently increasing the limit.
264
+
240
265
  ## AI agents and MCP
241
266
 
242
267
  Guard is deliberately model-agnostic. It does not send source code to a remote LLM and does not
@@ -283,6 +308,28 @@ task finished
283
308
  For Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, and generic hosts, see
284
309
  [`docs/integrations/`](docs/integrations/).
285
310
 
311
+ For workflows where the authoring agent must not approve its own policy proposals, set Guard to
312
+ protected mode in `rules.json`:
313
+
314
+ ```json
315
+ {
316
+ "approval": {
317
+ "mode": "protected",
318
+ "allowMcpApproval": false,
319
+ "requireDistinctActor": true
320
+ }
321
+ }
322
+ ```
323
+
324
+ MCP can then read and propose policy, while approval happens through the CLI or a protected CI/PR
325
+ process. With `requireDistinctActor`, CLI and permitted MCP approvals require `GUARD_APPROVER`,
326
+ and Guard rejects the same declared actor when a proposal was generated with `GUARD_PROPOSER`.
327
+ These variables provide declared provenance only; external branch protection or signed identity
328
+ remains responsible for proving who approved the change.
329
+
330
+ See [the extension map](docs/extensions.md) for the supported AI-host, CI/PR, policy-pack, tool
331
+ adapter, approval-governance, and observability integrations.
332
+
286
333
  ## CI
287
334
 
288
335
  Copy the consumer workflow into a project that has installed and initialized Guard:
@@ -319,8 +366,18 @@ real project shapes.
319
366
  Use `analyze --refresh` after a structural change. `doctor --fix-cache` removes only the disposable
320
367
  discovery cache; it does not change rules, contracts, baseline, or source files.
321
368
 
369
+ Guard state is safe for concurrent CLI/MCP readers and writers: derived facts are published as an
370
+ atomic generation, policy updates use revisions, and short write contention is waited out. A
371
+ process that dies while holding the state lock is detected by its local PID and the lock is
372
+ recovered; use `--no-wait` when an integration needs immediate contention feedback.
373
+
322
374
  Run `pnpm exec codapult-guard <command> --help` for command-specific options.
323
375
 
376
+ Every `verify` run also writes a local manifest under
377
+ `.codapult/guard/history/runs/<run-id>.json`. It records stage durations, the outcome, and the
378
+ policy gate that decided the result. It contains no source code or external telemetry. These
379
+ manifests make a failed run explainable without turning Guard into a production tracing system.
380
+
324
381
  ## Security and data handling
325
382
 
326
383
  - Deterministic discovery and checks run locally.
@@ -5,17 +5,20 @@ export interface CommandResult {
5
5
  exitCode: number;
6
6
  stdout: string;
7
7
  stderr: string;
8
- timedOut?: boolean;
9
- truncated?: boolean;
8
+ durationMs: number;
9
+ timedOut?: boolean | undefined;
10
+ truncated?: boolean | undefined;
10
11
  }
11
- export declare function runProjectCommand(command: string, cwd: string, options?: {
12
- timeout?: number;
13
- env?: NodeJS.ProcessEnv;
14
- }): CommandResult;
15
- export declare function commandResponse(result: CommandResult): {
12
+ export interface RunProjectCommandOptions {
13
+ timeout?: number | undefined;
14
+ env?: NodeJS.ProcessEnv | undefined;
15
+ }
16
+ export interface CommandResponse {
16
17
  content: {
17
18
  type: 'text';
18
19
  text: string;
19
20
  }[];
20
21
  isError: boolean;
21
- };
22
+ }
23
+ export declare function runProjectCommand(command: string, cwd: string, options?: RunProjectCommandOptions): CommandResult;
24
+ export declare function commandResponse(result: CommandResult): CommandResponse;
@@ -24,6 +24,7 @@ function parseCommand(command) {
24
24
  return { executable: windowsExecutable, args };
25
25
  }
26
26
  export function runProjectCommand(command, cwd, options = {}) {
27
+ const startedAt = Date.now();
27
28
  const parsed = parseCommand(command);
28
29
  if (!parsed) {
29
30
  return {
@@ -33,6 +34,7 @@ export function runProjectCommand(command, cwd, options = {}) {
33
34
  exitCode: 2,
34
35
  stdout: '',
35
36
  stderr: 'Unsafe or empty project command rejected.',
37
+ durationMs: 0,
36
38
  };
37
39
  }
38
40
  try {
@@ -51,6 +53,7 @@ export function runProjectCommand(command, cwd, options = {}) {
51
53
  exitCode: 0,
52
54
  stdout: captured.value,
53
55
  stderr: '',
56
+ durationMs: Math.max(0, Date.now() - startedAt),
54
57
  truncated: captured.truncated,
55
58
  };
56
59
  }
@@ -68,6 +71,7 @@ export function runProjectCommand(command, cwd, options = {}) {
68
71
  exitCode,
69
72
  stdout: stdout.value,
70
73
  stderr: timedOut ? `${stderr.value}\n[command timed out]` : stderr.value,
74
+ durationMs: Math.max(0, Date.now() - startedAt),
71
75
  timedOut,
72
76
  truncated: stdout.truncated || stderr.truncated,
73
77
  };
@@ -1,20 +1,33 @@
1
1
  import { type CommandResult } from './command.js';
2
- export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
3
- export type GuardAdapter = 'dependency-graph' | 'security' | 'dependency-hygiene';
4
- export type ExternalToolMode = 'auto' | 'on' | 'off';
2
+ import type { GuardAdapterName, GuardToolMode, ProjectCheck } from '../core/model/types.js';
3
+ export type { ProjectCheck } from '../core/model/types.js';
4
+ export type GuardAdapter = GuardAdapterName;
5
+ export type ExternalToolMode = GuardToolMode;
5
6
  export type ProjectCheckResults = Partial<Record<ProjectCheck, CommandResult>>;
7
+ export interface ProjectChecksOptions {
8
+ timeout?: number | undefined;
9
+ }
10
+ export interface WorkspaceProjectChecksOptions extends ProjectChecksOptions {
11
+ rootResults?: ProjectCheckResults | undefined;
12
+ }
13
+ export interface ProjectAdaptersOptions {
14
+ mode?: Exclude<ExternalToolMode, 'off'> | undefined;
15
+ timeout?: number | undefined;
16
+ tooling?: Partial<Record<GuardAdapter, {
17
+ script: string;
18
+ enabled: boolean;
19
+ }>> | undefined;
20
+ }
6
21
  export interface ProjectRuntimeDiagnostics {
7
22
  node: string;
8
23
  packageManager: string;
9
- declaredPackageManager?: string;
10
- declaredNode?: string;
24
+ declaredPackageManager?: string | undefined;
25
+ declaredNode?: string | undefined;
11
26
  compatible: boolean;
12
27
  issues: string[];
13
28
  }
14
29
  export declare function inspectProjectRuntime(root: string): ProjectRuntimeDiagnostics;
15
- export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?: {
16
- timeout?: number;
17
- }): ProjectCheckResults;
30
+ export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?: ProjectChecksOptions): ProjectCheckResults;
18
31
  /**
19
32
  * Runs checks for workspace packages only when the root package does not own
20
33
  * the corresponding check. Root orchestration remains the source of truth.
@@ -22,16 +35,6 @@ export declare function runProjectChecks(root: string, checks: ProjectCheck[], o
22
35
  export declare function runWorkspaceProjectChecks(root: string, packages: {
23
36
  path: string;
24
37
  scripts: Record<string, string>;
25
- }[], checks: ProjectCheck[], options?: {
26
- timeout?: number;
27
- rootResults?: ProjectCheckResults;
28
- }): Record<string, ProjectCheckResults>;
38
+ }[], checks: ProjectCheck[], options?: WorkspaceProjectChecksOptions): Record<string, ProjectCheckResults>;
29
39
  /** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
30
- export declare function runProjectAdapters(root: string, options?: {
31
- mode?: Exclude<ExternalToolMode, 'off'>;
32
- timeout?: number;
33
- tooling?: Partial<Record<GuardAdapter, {
34
- script: string;
35
- enabled: boolean;
36
- }>>;
37
- }): Partial<Record<GuardAdapter, CommandResult>>;
40
+ export declare function runProjectAdapters(root: string, options?: ProjectAdaptersOptions): Partial<Record<GuardAdapter, CommandResult>>;
@@ -12,6 +12,7 @@ function notConfigured(check) {
12
12
  exitCode: 0,
13
13
  stdout: '',
14
14
  stderr: `${check} script is not configured`,
15
+ durationMs: 0,
15
16
  };
16
17
  }
17
18
  function readPackageMetadata(root) {
@@ -119,6 +120,9 @@ const adapterScriptPatterns = {
119
120
  'dependency-graph': /(?:dep(?:endency)?[-:]?(?:check|graph|cruise)|madge|architecture)/i,
120
121
  security: /(?:security|semgrep|gitleaks|snyk|trivy|audit)/i,
121
122
  'dependency-hygiene': /(?:knip|dep(?:endency)?[-:]?(?:unused|hygiene))/i,
123
+ sast: /(?:semgrep|codeql|bearer|sast)/i,
124
+ 'secret-scanning': /(?:gitleaks|trufflehog|secret[-_ ]?scan)/i,
125
+ 'dependency-audit': /(?:npm audit|pnpm audit|yarn audit|osv|snyk|trivy|dependency[-_ ]?audit)/i,
122
126
  };
123
127
  /** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
124
128
  export function runProjectAdapters(root, options = {}) {
@@ -126,6 +130,7 @@ export function runProjectAdapters(root, options = {}) {
126
130
  const manager = packageManager(root, metadata.packageManager);
127
131
  const scripts = metadata.scripts ?? {};
128
132
  const results = {};
133
+ const executed = new Map();
129
134
  for (const [adapter, pattern] of Object.entries(adapterScriptPatterns)) {
130
135
  const configured = options.tooling?.[adapter];
131
136
  const script = configured?.enabled
@@ -141,9 +146,13 @@ export function runProjectAdapters(root, options = {}) {
141
146
  continue;
142
147
  }
143
148
  const command = manager === 'npm' ? `npm run ${script}` : `${manager} run ${script}`;
144
- results[adapter] = runProjectCommand(command, root, {
145
- timeout: options.timeout ?? 120_000,
146
- });
149
+ const previous = executed.get(command);
150
+ const result = previous ??
151
+ runProjectCommand(command, root, {
152
+ timeout: options.timeout ?? 120_000,
153
+ });
154
+ executed.set(command, result);
155
+ results[adapter] = result;
147
156
  }
148
157
  return results;
149
158
  }
@@ -1,64 +1,58 @@
1
1
  import { type GuardToolMode } from '../../core/guard.js';
2
- export declare function guardInitCommand(options?: {
3
- force?: boolean;
4
- }): void;
5
- export declare function guardAnalyzeCommand(_options?: {
6
- refresh?: boolean;
7
- }): void;
8
- export declare function guardProposeCommand(options?: {
9
- json?: boolean;
10
- }): void;
11
- export declare function guardInstallAgentCommand(target?: string, options?: {
12
- json?: boolean;
13
- }): void;
14
- export declare function guardDoctorCommand(options?: {
15
- json?: boolean;
16
- fixCache?: boolean;
2
+ interface GuardOutputOptions {
3
+ json?: boolean | undefined;
4
+ }
5
+ interface GuardInitOptions {
6
+ force?: boolean | undefined;
7
+ wait?: boolean | undefined;
8
+ }
9
+ interface GuardAnalyzeOptions {
10
+ refresh?: boolean | undefined;
11
+ wait?: boolean | undefined;
12
+ }
13
+ interface GuardVerifyOptions extends GuardOutputOptions {
14
+ checks?: string | undefined;
15
+ changed?: boolean | undefined;
16
+ requirement?: string | undefined;
17
+ tools?: GuardToolMode | undefined;
18
+ strict?: boolean | undefined;
19
+ projectChecks?: boolean | undefined;
20
+ }
21
+ interface GuardCheckOptions extends GuardOutputOptions {
22
+ changed?: boolean | undefined;
23
+ sarif?: boolean | undefined;
24
+ }
25
+ interface GuardBaselineOptions extends GuardOutputOptions {
26
+ all?: boolean | undefined;
27
+ reason?: string | undefined;
28
+ }
29
+ interface GuardApprovalOptions {
30
+ all?: boolean | undefined;
31
+ }
32
+ interface GuardReviewOptions {
33
+ maxDiffChars?: string | undefined;
34
+ requirement?: string | undefined;
35
+ base?: string | undefined;
36
+ }
37
+ export declare function guardInitCommand(options?: GuardInitOptions): void;
38
+ export declare function guardAnalyzeCommand(options?: GuardAnalyzeOptions): void;
39
+ export declare function guardProposeCommand(options?: GuardOutputOptions): void;
40
+ export declare function guardInstallAgentCommand(target?: string, options?: GuardOutputOptions): void;
41
+ export declare function guardDoctorCommand(options?: GuardOutputOptions & {
42
+ fixCache?: boolean | undefined;
17
43
  }): void;
18
44
  export declare function guardHistoryCommand(): void;
19
- export declare function guardImpactCommand(files: string[], options?: {
20
- json?: boolean;
21
- }): void;
22
- export declare function guardHistoryDiffCommand(from: string, to: string, options?: {
23
- json?: boolean;
24
- }): void;
25
- export declare function guardVerifyCommand(options?: {
26
- checks?: string;
27
- changed?: boolean;
28
- json?: boolean;
29
- requirement?: string;
30
- tools?: GuardToolMode;
31
- strict?: boolean;
32
- projectChecks?: boolean;
33
- }): void;
34
- export declare function guardCheckCommand(options?: {
35
- changed?: boolean;
36
- json?: boolean;
37
- sarif?: boolean;
38
- }): void;
39
- export declare function guardAuditCommand(options?: {
40
- json?: boolean;
41
- }): void;
42
- export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: {
43
- all?: boolean;
44
- reason?: string;
45
- json?: boolean;
46
- }): void;
47
- export declare function guardRulesApproveCommand(ids?: string, options?: {
48
- all?: boolean;
49
- }): void;
50
- export declare function guardPolicyExplainCommand(id: string, options?: {
51
- json?: boolean;
52
- }): void;
53
- export declare function guardContractsApproveCommand(ids?: string, options?: {
54
- all?: boolean;
55
- }): void;
56
- export declare function guardContractsRejectCommand(ids?: string, options?: {
57
- all?: boolean;
58
- }): void;
59
- export declare function guardReviewCommand(options?: {
60
- maxDiffChars?: string;
61
- requirement?: string;
62
- base?: string;
63
- }): void;
45
+ export declare function guardRunsCommand(options?: GuardOutputOptions): void;
46
+ export declare function guardImpactCommand(files: string[], options?: GuardOutputOptions): void;
47
+ export declare function guardHistoryDiffCommand(from: string, to: string, options?: GuardOutputOptions): void;
48
+ export declare function guardVerifyCommand(options?: GuardVerifyOptions): void;
49
+ export declare function guardCheckCommand(options?: GuardCheckOptions): void;
50
+ export declare function guardAuditCommand(options?: GuardOutputOptions): void;
51
+ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: GuardBaselineOptions): void;
52
+ export declare function guardRulesApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
53
+ export declare function guardPolicyExplainCommand(id: string, options?: GuardOutputOptions): void;
54
+ export declare function guardContractsApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
55
+ export declare function guardContractsRejectCommand(ids?: string, options?: GuardApprovalOptions): void;
56
+ export declare function guardReviewCommand(options?: GuardReviewOptions): void;
64
57
  export declare function guardIsInitialized(root: string): boolean;
58
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { relative, resolve } from 'node:path';
3
- import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, writeGuardConfig, writeGuardProposals, recordGuardProposalDecision, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardContracts, writeProjectModel, writeGuardMemory, writeProjectSnapshot, } from '../../core/guard.js';
3
+ import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardPolicy, writeProjectState, } from '../../core/guard.js';
4
4
  import { guardErrorPayload } from '../../core/errors.js';
5
5
  import { runGuardVerification, } from '../../core/verification/verify.js';
6
6
  import { diagnoseGuard } from '../../core/analysis/doctor.js';
@@ -9,6 +9,7 @@ import { guardAgentTargets, installGuardAgentInstructions, installGuardAgentTarg
9
9
  import { dim, fail, heading, info, success, warn } from '../ui.js';
10
10
  import { guardFindingsToSarif } from '../../core/output/sarif.js';
11
11
  import { analyzeProjectImpact } from '../../core/analysis/impact.js';
12
+ import { listGuardRuns, summarizeGuardRuns } from '../../core/history/runs.js';
12
13
  function renderFindings(findings) {
13
14
  for (const finding of findings) {
14
15
  const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
@@ -70,7 +71,7 @@ export function guardInitCommand(options = {}) {
70
71
  const root = getRoot();
71
72
  let initialized;
72
73
  try {
73
- initialized = initializeGuard(root, { force: options.force });
74
+ initialized = initializeGuard(root, options.wait === false ? { force: options.force, noWait: true } : { force: options.force });
74
75
  }
75
76
  catch (error) {
76
77
  if (error instanceof GuardAlreadyInitializedError) {
@@ -97,14 +98,12 @@ export function guardInitCommand(options = {}) {
97
98
  info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
98
99
  dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
99
100
  }
100
- export function guardAnalyzeCommand(_options = {}) {
101
+ export function guardAnalyzeCommand(options = {}) {
101
102
  const root = getRoot();
102
103
  const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
103
104
  persistCache: true,
104
105
  });
105
- writeProjectModel(root, projectModel);
106
- writeGuardMemory(root, projectModel);
107
- const revision = writeProjectSnapshot(root, projectModel);
106
+ const revision = writeProjectState(root, projectModel, options.wait === false ? { noWait: true } : undefined);
108
107
  heading('Codapult Guard Analyze');
109
108
  success(`Updated ${GUARD_PROJECT_FILE}, ${GUARD_ARCHITECTURE_FILE}, and ${GUARD_CONVENTIONS_FILE}`);
110
109
  info(`${projectModel.files.length} file(s), ${projectModel.modules.length} module(s), ${projectModel.insights.cycles.length} cycle(s) recorded as project context.`);
@@ -196,6 +195,19 @@ export function guardHistoryCommand() {
196
195
  info(`${snapshot.revision}: ${snapshot.files} files, ${snapshot.modules} modules, ${snapshot.cycles} cycles`);
197
196
  }
198
197
  }
198
+ export function guardRunsCommand(options = {}) {
199
+ const root = getRoot();
200
+ const result = { summary: summarizeGuardRuns(root), runs: listGuardRuns(root) };
201
+ if (options.json) {
202
+ console.log(JSON.stringify(result, null, 2));
203
+ return;
204
+ }
205
+ heading('Codapult Guard Runs');
206
+ info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
207
+ for (const run of result.runs) {
208
+ info(`${run.runId}: ${run.outcome}, gate=${run.gate}, ${run.durationMs} ms`);
209
+ }
210
+ }
199
211
  export function guardImpactCommand(files, options = {}) {
200
212
  const root = getRoot();
201
213
  if (files.length === 0) {
@@ -326,7 +338,7 @@ export function guardCheckCommand(options = {}) {
326
338
  baseline: loadBaseline(root),
327
339
  includeArchitectureInsights: true,
328
340
  });
329
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
341
+ const contractIssues = validateGuardPolicy(root, config);
330
342
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
331
343
  const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
332
344
  const contractFindings = contractIssues.map((issue) => ({
@@ -392,7 +404,7 @@ export function guardAuditCommand(options = {}) {
392
404
  return;
393
405
  }
394
406
  const report = scanGuard(root, config, { includeArchitectureInsights: true });
395
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
407
+ const contractIssues = validateGuardPolicy(root, config);
396
408
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
397
409
  const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
398
410
  if (options.json) {
@@ -491,11 +503,16 @@ export function guardRulesApproveCommand(ids, options = {}) {
491
503
  return;
492
504
  }
493
505
  const selectedIds = new Set(selected.map((rule) => rule.id));
494
- writeGuardConfig(root, {
506
+ const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
507
+ if (approvalError) {
508
+ fail(approvalError);
509
+ process.exitCode = 1;
510
+ return;
511
+ }
512
+ applyGuardProposalDecision(root, {
495
513
  ...config,
496
514
  rules: config.rules.map((rule) => selectedIds.has(rule.id) ? { ...rule, status: 'active' } : rule),
497
- });
498
- recordGuardProposalDecision(root, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })));
515
+ }, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })), { source: 'cli' });
499
516
  for (const rule of selected)
500
517
  success(`Activated ${rule.id}`);
501
518
  process.exitCode = 0;
@@ -566,15 +583,20 @@ export function guardContractsApproveCommand(ids, options = {}) {
566
583
  return;
567
584
  }
568
585
  const selectedIds = new Set(selected.map((contract) => contract.id));
569
- writeGuardConfig(root, {
586
+ const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
587
+ if (approvalError) {
588
+ fail(approvalError);
589
+ process.exitCode = 1;
590
+ return;
591
+ }
592
+ applyGuardProposalDecision(root, {
570
593
  ...config,
571
594
  contracts: (config.contracts ?? []).map((contract) => selectedIds.has(contract.id) ? { ...contract, status: 'active' } : contract),
572
- });
573
- recordGuardProposalDecision(root, selected.map((contract) => ({
595
+ }, selected.map((contract) => ({
574
596
  id: contract.id,
575
597
  type: 'contract',
576
598
  decision: 'approved',
577
- })));
599
+ })), { source: 'cli' });
578
600
  for (const contract of selected)
579
601
  success(`Activated ${contract.id}`);
580
602
  process.exitCode = 0;
@@ -601,15 +623,20 @@ export function guardContractsRejectCommand(ids, options = {}) {
601
623
  process.exitCode = 1;
602
624
  return;
603
625
  }
604
- writeGuardConfig(root, {
626
+ const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
627
+ if (approvalError) {
628
+ fail(approvalError);
629
+ process.exitCode = 1;
630
+ return;
631
+ }
632
+ applyGuardProposalDecision(root, {
605
633
  ...config,
606
634
  contracts: config.contracts ?? [],
607
- });
608
- recordGuardProposalDecision(root, selected.map((contract) => ({
635
+ }, selected.map((contract) => ({
609
636
  id: contract.id,
610
637
  type: 'contract',
611
638
  decision: 'rejected',
612
- })));
639
+ })), { source: 'cli' });
613
640
  for (const contract of selected)
614
641
  success(`Rejected ${contract.id}`);
615
642
  process.exitCode = 0;
package/dist/cli/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
- import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
4
+ import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
5
5
  import { config } from '../core/config.js';
6
- import { GuardConfigError, GuardStateBusyError } from '../core/guard.js';
6
+ import { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
7
7
  import { guardErrorPayload } from '../core/errors.js';
8
8
  const program = new Command()
9
9
  .name(config.commandName)
@@ -16,12 +16,13 @@ const program = new Command()
16
16
  // Guard is the standalone product, so its commands live at the package root:
17
17
  // `codapult-guard init`, not `codapult-guard guard init`.
18
18
  const guard = program;
19
- guard.command('init').option('--force').action(guardInitCommand);
20
- guard.command('analyze').option('--refresh').action(guardAnalyzeCommand);
19
+ guard.command('init').option('--force').option('--no-wait').action(guardInitCommand);
20
+ guard.command('analyze').option('--refresh').option('--no-wait').action(guardAnalyzeCommand);
21
21
  guard.command('propose').option('--json').action(guardProposeCommand);
22
22
  guard.command('install-agent [target]').option('--json').action(guardInstallAgentCommand);
23
23
  guard.command('doctor').option('--json').option('--fix-cache').action(guardDoctorCommand);
24
24
  guard.command('history').action(guardHistoryCommand);
25
+ guard.command('runs').option('--json').action(guardRunsCommand);
25
26
  guard.command('history-diff <from> <to>').option('--json').action(guardHistoryDiffCommand);
26
27
  guard.command('impact <files...>').option('--json').action(guardImpactCommand);
27
28
  guard
@@ -81,15 +82,28 @@ try {
81
82
  program.parse();
82
83
  }
83
84
  catch (error) {
84
- if (error instanceof GuardConfigError || error instanceof GuardStateBusyError) {
85
+ if (error instanceof GuardConfigError ||
86
+ error instanceof GuardBaselineReasonError ||
87
+ error instanceof GuardStateBusyError ||
88
+ error instanceof GuardStateStaleError) {
85
89
  const isBusy = error instanceof GuardStateBusyError;
86
- console.error(JSON.stringify(guardErrorPayload(isBusy ? 'GUARD_STATE_BUSY' : 'GUARD_CONFIG_INVALID', error.message, {
87
- configured: !isBusy,
90
+ const isStale = error instanceof GuardStateStaleError;
91
+ const isInvalidInput = error instanceof GuardBaselineReasonError;
92
+ console.error(JSON.stringify(guardErrorPayload(isBusy
93
+ ? 'GUARD_STATE_BUSY'
94
+ : isStale
95
+ ? 'GUARD_STATE_STALE'
96
+ : isInvalidInput
97
+ ? 'GUARD_INVALID_INPUT'
98
+ : 'GUARD_CONFIG_INVALID', error.message, {
99
+ configured: !isBusy && !isStale,
88
100
  outcome: 'error',
89
101
  recoverable: true,
90
- hint: isBusy
91
- ? 'Retry after the other Guard process finishes.'
92
- : 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
102
+ hint: isBusy || isStale
103
+ ? 'Re-read Guard state and retry the operation.'
104
+ : isInvalidInput
105
+ ? 'Provide a written reason for the baseline decision.'
106
+ : 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
93
107
  }), null, 2));
94
108
  process.exitCode = 1;
95
109
  }
@@ -7,6 +7,6 @@ export interface GuardDoctorReport {
7
7
  status: 'ok' | 'warning' | 'fail';
8
8
  initialized: boolean;
9
9
  items: GuardDoctorItem[];
10
- recommendation?: string;
10
+ recommendation?: string | undefined;
11
11
  }
12
12
  export declare function diagnoseGuard(root: string): GuardDoctorReport;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { GUARD_AGENT_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_BASELINE_FILE, GUARD_CONVENTIONS_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_RULES_FILE, loadGuardConfig, loadProjectModel, isGuardAgentConfig, isGuardContractsFile, isGuardProposalFile, validateGuardContracts, } from '../guard.js';
3
+ import { GUARD_AGENT_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_BASELINE_FILE, GUARD_CONVENTIONS_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_RULES_FILE, loadGuardConfig, loadProjectModel, isGuardAgentConfig, isGuardContractsFile, isGuardProposalFile, validateGuardPolicy, } from '../guard.js';
4
4
  const requiredArtifacts = [
5
5
  GUARD_BASELINE_FILE,
6
6
  GUARD_RULES_FILE,
@@ -48,8 +48,7 @@ export function diagnoseGuard(root) {
48
48
  (() => {
49
49
  try {
50
50
  const guardConfig = loadGuardConfig(root);
51
- return (guardConfig !== undefined &&
52
- validateGuardContracts(root, guardConfig.contracts).length > 0);
51
+ return guardConfig !== undefined && validateGuardPolicy(root, guardConfig).length > 0;
53
52
  }
54
53
  catch {
55
54
  return true;