dw-kit 1.9.3 → 1.10.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.
@@ -0,0 +1,113 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/dv-workflow/dv-workflow/schemas/overnight-digest.schema.json",
4
+ "title": "DW Read-Contract: overnight-digest@v1",
5
+ "description": "Machine-readable summary of one Overnight Autopilot run (ADR-0023). The human-facing OVERNIGHT.md is rendered from this shape; external dashboards/adapters consume it. Produced by summarizeRun() over .dw/events-global.jsonl.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "counts", "stopReason"],
9
+ "properties": {
10
+ "schema_version": { "type": "string", "const": "overnight-digest@v1" },
11
+ "date": { "type": "string", "description": "ISO date (YYYY-MM-DD) of the run" },
12
+ "goal": { "type": ["string", "null"], "description": "Goal id/title the run pursued" },
13
+ "counts": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["done", "parked", "hardStops", "autoApproved"],
17
+ "properties": {
18
+ "done": { "type": "integer", "minimum": 0 },
19
+ "parked": { "type": "integer", "minimum": 0 },
20
+ "hardStops": { "type": "integer", "minimum": 0 },
21
+ "autoApproved": { "type": "integer", "minimum": 0 }
22
+ }
23
+ },
24
+ "stopReason": {
25
+ "type": "string",
26
+ "description": "Why the run stopped",
27
+ "examples": ["budget", "done", "no-progress", "unknown"]
28
+ },
29
+ "wrapupSeen": {
30
+ "type": "boolean",
31
+ "description": "Whether an overnight_wrapup event was present (false = run may have crashed)"
32
+ },
33
+ "done": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "object",
37
+ "properties": {
38
+ "subtask": { "type": "string" },
39
+ "commit": { "type": "string" },
40
+ "summary": { "type": "string" }
41
+ }
42
+ }
43
+ },
44
+ "autoApproved": {
45
+ "type": "array",
46
+ "description": "Gates Trust Mode waved (event: gate_auto_approved)",
47
+ "items": {
48
+ "type": "object",
49
+ "properties": {
50
+ "gate": { "type": "string" },
51
+ "task": { "type": ["string", "null"] },
52
+ "reason": { "type": "string" },
53
+ "evidence": {
54
+ "type": "object",
55
+ "description": "EVD verify — the basis cited for the auto-approval, not just the outcome",
56
+ "properties": {
57
+ "tests": {
58
+ "type": "object",
59
+ "properties": {
60
+ "command": { "type": "string" },
61
+ "passed": { "type": "integer" },
62
+ "failed": { "type": "integer" },
63
+ "exit": { "type": "integer" }
64
+ }
65
+ },
66
+ "review": {
67
+ "type": "object",
68
+ "properties": {
69
+ "critical": { "type": "integer", "minimum": 0 },
70
+ "warning": { "type": "integer", "minimum": 0 },
71
+ "suggestion": { "type": "integer", "minimum": 0 }
72
+ }
73
+ },
74
+ "diff": {
75
+ "type": "object",
76
+ "properties": {
77
+ "files": { "type": "integer" },
78
+ "insertions": { "type": "integer" },
79
+ "deletions": { "type": "integer" },
80
+ "within_scope": { "type": "boolean" }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ },
88
+ "parked": {
89
+ "type": "array",
90
+ "description": "Decisions the agent refused to guess on — the human acts on these",
91
+ "items": {
92
+ "type": "object",
93
+ "properties": {
94
+ "item": { "type": "string" },
95
+ "question": { "type": "string" },
96
+ "options": { "type": "array", "items": { "type": "string" } }
97
+ }
98
+ }
99
+ },
100
+ "hardStops": {
101
+ "type": "array",
102
+ "description": "Guard / Critical blocks that fired",
103
+ "items": {
104
+ "type": "object",
105
+ "properties": {
106
+ "gate": { "type": "string" },
107
+ "blocker": { "type": "string" },
108
+ "reason": { "type": "string" }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Overnight Autopilot recipe (ADR-0023). Scheduling lives in YOUR cron/CI — the
3
+ # toolkit ships no daemon (ADR-0021). Copy, set the two vars, add to crontab:
4
+ # 0 1 * * * /path/to/autopilot-cron.sh >> ~/dw-autopilot.log 2>&1
5
+ set -euo pipefail
6
+
7
+ PROJECT_DIR="${PROJECT_DIR:-$HOME/your-project}"
8
+ GOAL_ID="${GOAL_ID:-G-your-goal}"
9
+ MAX_ITER="${MAX_ITER:-8}"
10
+
11
+ cd "$PROJECT_DIR"
12
+
13
+ # INPUT contract — refuse to start in an unsafe state (dirty tree, no budget, …).
14
+ if ! dw autopilot preflight --goal "$GOAL_ID" --max-iter "$MAX_ITER"; then
15
+ echo "autopilot: preflight failed — not running tonight."
16
+ exit 0
17
+ fi
18
+
19
+ # Drive the goal unattended with Trust Mode. Gates auto-approve per your config;
20
+ # Guards + Critical findings + GATE B still hard-stop. Safe-park on ambiguity.
21
+ dw goal --auto=full --trust --max-iter "$MAX_ITER" "$GOAL_ID" || true
22
+
23
+ # OUTPUT contract — verify the end-state, then render the morning digest.
24
+ dw autopilot postflight --require-wrapup || true # verdict is still written below
25
+ dw overnight digest --goal "$GOAL_ID"
26
+
27
+ # NEVER push here. Local commits only — review OVERNIGHT.md and push yourself.
28
+ echo "autopilot: done — review OVERNIGHT.md in $PROJECT_DIR"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dw-kit
2
2
 
3
- 🇬🇧 **English** · [🇻🇳 Tiếng Việt](README.vi.md)
3
+ 🇬🇧 **English** · [🇻🇳 Tiếng Việt](README-vi.md)
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dw-kit?color=cb3837&logo=npm)](https://www.npmjs.com/package/dw-kit)
6
6
  [![downloads](https://img.shields.io/npm/dm/dw-kit?color=blue)](https://www.npmjs.com/package/dw-kit)
@@ -172,6 +172,8 @@ dw upgrade [--check] # update toolkit files (keeps your customizations)
172
172
  dw task new <name> # scaffold a v3 task doc
173
173
  dw task show [name] # ANSI snapshot of a task
174
174
  dw goal status # Goals → Key Results progress
175
+ dw trust status # Trust Mode — opt-in gate auto-approval (ADR-0022, off by default)
176
+ dw autopilot preflight # gate an unattended overnight run (ADR-0023); + `dw overnight digest`
175
177
  dw security-scan # AI-native supply-chain guard (OSV + heuristics)
176
178
  dw dashboard # active tasks + ADRs + telemetry summary
177
179
  dw metrics # local-only telemetry (opt out: DW_NO_TELEMETRY=1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dw-kit",
3
- "version": "1.9.3",
3
+ "version": "1.10.0",
4
4
  "description": "AI development workflow toolkit — structured, quality-assured, team-ready. From requirements to dashboard.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -219,6 +219,69 @@ export function run(argv) {
219
219
  await decisionIndexCommand(opts);
220
220
  });
221
221
 
222
+ const trustCmd = program
223
+ .command('trust')
224
+ .description('Trust Mode (ADR-0022): opt-in auto-approval for dw:flow convenience gates — Guards + Critical never relaxed');
225
+
226
+ trustCmd
227
+ .command('status')
228
+ .description('Show resolved Trust Mode state for this project')
229
+ .action(async () => {
230
+ const { trustStatusCommand } = await import('./commands/trust.mjs');
231
+ await trustStatusCommand();
232
+ });
233
+
234
+ trustCmd
235
+ .command('gate <name>')
236
+ .description('Deterministic gate decision (scope|plan|changes|arch). Exit 0 = auto-approve, 10 = manual stop')
237
+ .option('--trust <gates>', 'One-shot trust for this call: true | comma-separated gate ids')
238
+ .option('--critical', 'A Critical review finding exists (forces GATE D manual)')
239
+ .option('--task <name>', 'Task the gate belongs to (recorded in the audit event)')
240
+ .option('--evidence <json>', 'EVD verify: JSON basis, e.g. {"tests":{"exit":0},"review":{"critical":0}}')
241
+ .option('--run-count <n>', 'Auto-approvals so far this run (enforces workflow.trust.max_auto_subtasks)')
242
+ .action(async (name, opts) => {
243
+ const { trustGateCommand } = await import('./commands/trust.mjs');
244
+ await trustGateCommand(name, opts);
245
+ });
246
+
247
+ const autopilotCmd = program
248
+ .command('autopilot')
249
+ .description('Overnight Autopilot contract (ADR-0023): preflight (INPUT) + postflight (OUTPUT) assertions');
250
+
251
+ autopilotCmd
252
+ .command('preflight')
253
+ .description('Assert it is safe to START an unattended run (clean tree · budget · trust bounded · tests · goal). Exit 0 = clear, 1 = blocked')
254
+ .option('--goal <id>', 'Goal id the run will pursue')
255
+ .option('--max-iter <n>', 'Iteration budget for this run')
256
+ .action(async (opts) => {
257
+ const { autopilotPreflightCommand } = await import('./commands/autopilot.mjs');
258
+ await autopilotPreflightCommand(opts);
259
+ });
260
+
261
+ autopilotCmd
262
+ .command('postflight')
263
+ .description('Assert the run ended in a safe, reviewable state (digest written · wrap-up · local-only). Exit 0 = clean, 1 = unsafe')
264
+ .option('--require-wrapup', 'Treat a missing overnight_wrapup event as a blocking failure')
265
+ .action(async (opts) => {
266
+ const { autopilotPostflightCommand } = await import('./commands/autopilot.mjs');
267
+ await autopilotPostflightCommand(opts);
268
+ });
269
+
270
+ const overnightCmd = program
271
+ .command('overnight')
272
+ .description('Overnight Autopilot (ADR-0023): morning-review digest for unattended runs');
273
+
274
+ overnightCmd
275
+ .command('digest')
276
+ .description('Render OVERNIGHT.md from recent .dw/events-global.jsonl run events')
277
+ .option('--stdout', 'Print to stdout instead of writing OVERNIGHT.md')
278
+ .option('--limit <n>', 'How many recent events to scan (default 500)')
279
+ .option('--goal <id>', 'Goal id/title the run pursued (for the header)')
280
+ .action(async (opts) => {
281
+ const { overnightDigestCommand } = await import('./commands/overnight.mjs');
282
+ await overnightDigestCommand(opts);
283
+ });
284
+
222
285
  const agentCmd = program
223
286
  .command('agent')
224
287
  .description('Agent OS multi-agent orchestration (ADR-0009): claim · release · renew · expire · claims · reports · conflicts · check-staged · verify');
@@ -0,0 +1,65 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { header, ok, warn, err, log } from '../lib/ui.mjs';
4
+ import { loadConfigWithLocal } from '../lib/config.mjs';
5
+ import { checkPreconditions, checkPostconditions } from '../lib/autopilot-contract.mjs';
6
+ import { readGoalEvents } from '../lib/goal-events.mjs';
7
+
8
+ function loadProjectConfig(projectDir) {
9
+ const configDir = join(projectDir, '.dw', 'config');
10
+ if (!existsSync(join(configDir, 'dw.config.yml'))) return null;
11
+ return loadConfigWithLocal(configDir);
12
+ }
13
+
14
+ function renderChecks(checks) {
15
+ for (const c of checks) {
16
+ const mark = c.ok ? ok : (c.severity === 'block' ? err : warn);
17
+ mark(`[${c.severity}] ${c.id} — ${c.detail}`);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * `dw autopilot preflight` — assert it is safe to START an unattended run
23
+ * (ADR-0023 INPUT contract). Exit 0 = clear to run, 1 = blocked.
24
+ */
25
+ export async function autopilotPreflightCommand(opts = {}) {
26
+ const rootDir = process.cwd();
27
+ const config = loadProjectConfig(rootDir) || {};
28
+ const maxIter = opts.maxIter != null ? Number(opts.maxIter) : null;
29
+ const { ok: passed, checks } = checkPreconditions({ rootDir, config, goalId: opts.goal || null, maxIter });
30
+
31
+ header('Autopilot preflight (ADR-0023 INPUT contract)');
32
+ renderChecks(checks);
33
+ log('');
34
+ if (passed) {
35
+ ok('Preconditions met — clear to start an unattended run.');
36
+ process.exit(0);
37
+ }
38
+ err('Preconditions FAILED — do not start. Fix the [block] items above.');
39
+ process.exit(1);
40
+ }
41
+
42
+ /**
43
+ * `dw autopilot postflight` — assert the run ended in a safe, reviewable state
44
+ * (ADR-0023 OUTPUT contract). Exit 0 = clean, 1 = unsafe end-state.
45
+ */
46
+ export async function autopilotPostflightCommand(opts = {}) {
47
+ const rootDir = process.cwd();
48
+ // Derive wrap-up presence from the event log unless caller asserts otherwise.
49
+ let wrapupSeen = null;
50
+ if (opts.requireWrapup) {
51
+ const events = readGoalEvents(rootDir, { limit: 500 });
52
+ wrapupSeen = events.some((e) => e && e.event === 'overnight_wrapup');
53
+ }
54
+ const { ok: passed, checks } = checkPostconditions({ rootDir, wrapupSeen });
55
+
56
+ header('Autopilot postflight (ADR-0023 OUTPUT contract)');
57
+ renderChecks(checks);
58
+ log('');
59
+ if (passed) {
60
+ ok('Postconditions met — run ended in a reviewable state.');
61
+ process.exit(0);
62
+ }
63
+ err('Postconditions FAILED — inspect manually before resuming.');
64
+ process.exit(1);
65
+ }
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { header, ok, warn, err, info, log } from '../lib/ui.mjs';
6
- import { loadConfig, getToolkitVersions, getReviewRendererConfig } from '../lib/config.mjs';
6
+ import { loadConfig, loadConfigWithLocal, getToolkitVersions, getReviewRendererConfig, getTrustConfig } from '../lib/config.mjs';
7
7
  import { detectPlatform, platformLabel } from '../lib/platform.mjs';
8
8
  import { snapshotInfo } from '../lib/sc-sync.mjs';
9
9
 
@@ -275,6 +275,22 @@ export async function doctorCommand() {
275
275
  }
276
276
  }
277
277
 
278
+ info('Trust Mode (ADR-0022, opt-in)');
279
+ // Read the effective (base + local override) config — a privacy-conscious user
280
+ // may enable Trust Mode only in dw.config.local.yml. (review round 2: W-4)
281
+ const trustCfg = existsSync(configPath)
282
+ ? getTrustConfig(loadConfigWithLocal(join(projectDir, '.dw', 'config')) || {})
283
+ : getTrustConfig({});
284
+ if (!trustCfg.enabled) {
285
+ log(' Status : OFF — all gates manual (default)');
286
+ } else {
287
+ const on = Object.entries(trustCfg.auto_approve).filter(([, v]) => v).map(([k]) => k);
288
+ warn(`Trust Mode ON — auto-approving: ${on.length ? on.join(', ') : '(none selected)'}`);
289
+ log(` Max subtasks: ${trustCfg.max_auto_subtasks === 0 ? 'unbounded' : trustCfg.max_auto_subtasks}`);
290
+ log(' Never relaxed: Guards, GATE D on Critical, GATE B (TL arch)');
291
+ warnings++;
292
+ }
293
+
278
294
  console.log();
279
295
  header('Diagnosis');
280
296
  if (issues === 0 && warnings === 0) {
@@ -0,0 +1,35 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { header, ok, log } from '../lib/ui.mjs';
4
+ import { readGoalEvents } from '../lib/goal-events.mjs';
5
+ import { buildDigest, buildDigestData, writeDigest, summarizeRun } from '../lib/overnight-digest.mjs';
6
+
7
+ /**
8
+ * `dw overnight digest` — render OVERNIGHT.md from recent run events (ADR-0023).
9
+ * Sourced deterministically from .dw/events-global.jsonl so the morning digest
10
+ * is reproducible, not free-form narration.
11
+ */
12
+ export async function overnightDigestCommand(opts = {}) {
13
+ const rootDir = process.cwd();
14
+ const limit = Number.isInteger(Number(opts.limit)) ? Number(opts.limit) : 500;
15
+ const events = readGoalEvents(rootDir, { limit });
16
+
17
+ const content = buildDigest({ events, goal: opts.goal || null });
18
+
19
+ if (opts.stdout) {
20
+ process.stdout.write(content + '\n');
21
+ process.exit(0);
22
+ }
23
+
24
+ const target = writeDigest(rootDir, content);
25
+ // Also emit the machine-readable read-contract (overnight-digest@v1) so
26
+ // dashboards/adapters consume the run without parsing markdown (ADR-0023).
27
+ const data = buildDigestData({ events, goal: opts.goal || null });
28
+ writeFileSync(join(rootDir, 'OVERNIGHT.json'), JSON.stringify(data, null, 2), 'utf8');
29
+ const s = summarizeRun(events);
30
+ header('Overnight digest');
31
+ ok(`Wrote ${target} + OVERNIGHT.json`);
32
+ log(`✅ ${s.counts.done} done · ⏸ ${s.counts.parked} parked · 🛑 ${s.counts.hardStops} hard-stops · ⚡ ${s.counts.autoApproved} auto-approved`);
33
+ if (s.counts.parked > 0) log('⏸ Parked items need your call — see the "Parked for you" section.');
34
+ process.exit(0);
35
+ }
@@ -0,0 +1,79 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { header, ok, warn, info, log } from '../lib/ui.mjs';
4
+ import { loadConfigWithLocal, getTrustConfig } from '../lib/config.mjs';
5
+ import { resolveGate, logTrustEvent, GATES } from '../lib/trust.mjs';
6
+
7
+ function loadProjectConfig(projectDir) {
8
+ const configDir = join(projectDir, '.dw', 'config');
9
+ if (!existsSync(join(configDir, 'dw.config.yml'))) return null;
10
+ return loadConfigWithLocal(configDir);
11
+ }
12
+
13
+ /**
14
+ * `dw trust status` — show the resolved Trust Mode state (ADR-0022).
15
+ */
16
+ export async function trustStatusCommand() {
17
+ const config = loadProjectConfig(process.cwd());
18
+ const trust = getTrustConfig(config || {});
19
+
20
+ header('Trust Mode (ADR-0022)');
21
+ if (!trust.enabled) {
22
+ log('Status : OFF — every gate is manual (default).');
23
+ log('Enable : set workflow.trust.enabled: true in .dw/config/dw.config.yml');
24
+ log('Or one-shot : /dw:flow --trust <task>');
25
+ process.exit(0);
26
+ }
27
+
28
+ warn('Status : ON — some convenience gates auto-approve.');
29
+ if (trust.profile) log(`Profile : ${trust.profile}`);
30
+ log(`Scope (A/Q1): ${trust.auto_approve.scope ? 'auto-approve' : 'manual'}`);
31
+ log(`Plan (C) : ${trust.auto_approve.plan ? 'auto-approve' : 'manual'}`);
32
+ log(`Changes (D) : ${trust.auto_approve.changes ? 'auto-approve (never on Critical)' : 'manual'}`);
33
+ log(`Max subtasks: ${trust.max_auto_subtasks === 0 ? 'unbounded' : trust.max_auto_subtasks}`);
34
+ log(`Require evid: ${trust.require_evidence ? 'yes — GATE D needs --evidence' : 'no'}`);
35
+ info('Never relaxed');
36
+ log('Guards (privacy-block, secret-scan), GATE D on Critical, GATE B (TL arch).');
37
+ process.exit(0);
38
+ }
39
+
40
+ /**
41
+ * `dw trust gate <name>` — deterministic gate decision the skill can call.
42
+ * Exit 0 = auto-approve, exit 10 = manual stop required.
43
+ */
44
+ export async function trustGateCommand(gate, opts = {}) {
45
+ const name = String(gate || '').toLowerCase();
46
+ if (!GATES[name]) {
47
+ warn(`Unknown gate "${gate}". Known: ${Object.keys(GATES).join(', ')}`);
48
+ process.exit(2);
49
+ }
50
+ const config = loadProjectConfig(process.cwd());
51
+ const evidence = parseEvidence(opts.evidence);
52
+ const runCount = opts.runCount != null && Number.isInteger(Number(opts.runCount)) ? Number(opts.runCount) : undefined;
53
+ const decision = resolveGate({
54
+ gate: name,
55
+ config: config || {},
56
+ flag: opts.trust,
57
+ hasCritical: opts.critical === true,
58
+ evidence,
59
+ runCount,
60
+ });
61
+
62
+ if (decision.autoApprove) {
63
+ // Invariant 4 (ADR-0022): every auto-approval is logged. This is the only
64
+ // code path that auto-approves, so the audit record is written HERE.
65
+ const res = logTrustEvent({ gate: name, task: opts.task || null, reason: decision.reason, evidence }, process.cwd());
66
+ ok(`${name}: AUTO-APPROVE — ${decision.reason}`);
67
+ if (!res.ok) warn(`(audit log failed: ${res.error?.message || 'unknown'} — proceeding)`);
68
+ process.exit(0);
69
+ }
70
+ warn(`${name}: MANUAL — ${decision.reason}`);
71
+ process.exit(10);
72
+ }
73
+
74
+ // Evidence is passed as a JSON string (EVD verify, ADR-0023 §evidence). Tolerant:
75
+ // bad/absent JSON → undefined (the gate then falls back to its no-evidence rule).
76
+ function parseEvidence(raw) {
77
+ if (!raw) return undefined;
78
+ try { return JSON.parse(raw); } catch { return undefined; }
79
+ }
@@ -0,0 +1,97 @@
1
+ // Input/Output contract for unattended autopilot runs (ADR-0023, review round 2).
2
+ //
3
+ // ADR-0023's "run contract" was prose in the skill — i.e. it held only if the
4
+ // LLM remembered it. This module makes the INPUT half (preconditions) and the
5
+ // OUTPUT half (postconditions) machine-checkable so an unattended run REFUSES to
6
+ // start in an unsafe state and its end-state is verified, not narrated.
7
+
8
+ import { execSync } from 'node:child_process';
9
+ import { existsSync as fileExists, statSync as fileStat } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { getTrustConfig } from './config.mjs';
12
+
13
+ function git(args, rootDir) {
14
+ try {
15
+ return execSync(`git ${args}`, { cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ function check(id, severity, ok, detail) {
22
+ return { id, severity, ok, detail };
23
+ }
24
+
25
+ /**
26
+ * INPUT contract — assert it is safe to START an unattended run.
27
+ * @returns {{ ok: boolean, checks: object[] }} ok=false if any `block` check fails
28
+ */
29
+ export function checkPreconditions({ rootDir = process.cwd(), config = {}, goalId = null, maxIter = null } = {}) {
30
+ const checks = [];
31
+ const trust = getTrustConfig(config);
32
+
33
+ // 1. clean tree — a dirty tree makes the night's diff un-attributable.
34
+ const porcelain = git('status --porcelain', rootDir);
35
+ checks.push(check(
36
+ 'clean-tree', 'block', porcelain === '',
37
+ porcelain === '' ? 'working tree clean' : `uncommitted changes present:\n${(porcelain || '(git unavailable)').split('\n').slice(0, 5).join('\n')}`,
38
+ ));
39
+
40
+ // 2. budget set — an unbounded unattended run is the footgun (ADR-0023 Force 3).
41
+ const bounded = (Number.isInteger(maxIter) && maxIter > 0) || trust.max_auto_subtasks > 0;
42
+ checks.push(check(
43
+ 'budget-set', 'block', bounded,
44
+ bounded ? `bounded (--max-iter=${maxIter ?? 'unset'}, max_auto_subtasks=${trust.max_auto_subtasks})` : 'no --max-iter and max_auto_subtasks=0 — run is unbounded',
45
+ ));
46
+
47
+ // 3. trust not blanket-unbounded — the "trust everything, forever" misconfig.
48
+ const blanket = trust.enabled && trust.auto_approve.scope && trust.auto_approve.plan && trust.auto_approve.changes;
49
+ const dangerous = blanket && trust.max_auto_subtasks === 0;
50
+ checks.push(check(
51
+ 'trust-bounded', 'block', !dangerous,
52
+ dangerous ? 'all gates trusted AND unbounded — set max_auto_subtasks or narrow trust' : 'trust scope is bounded',
53
+ ));
54
+
55
+ // 4. tests configured — establishes the green baseline the OUTPUT contract preserves.
56
+ const testCmd = config?.quality?.test_command || '';
57
+ checks.push(check(
58
+ 'tests-configured', 'warn', !!testCmd,
59
+ testCmd ? `quality.test_command = "${testCmd}"` : 'no quality.test_command — cannot assert a green baseline',
60
+ ));
61
+
62
+ // 5. goal defined — safe-park needs a definition of "done" to judge ambiguity.
63
+ checks.push(check(
64
+ 'goal-defined', 'warn', !!goalId,
65
+ goalId ? `goal: ${goalId}` : 'no goal id passed — pursuit loop has no measurable target',
66
+ ));
67
+
68
+ const ok = checks.filter((c) => c.severity === 'block').every((c) => c.ok);
69
+ return { ok, checks };
70
+ }
71
+
72
+ /**
73
+ * OUTPUT contract — assert the run ended in a safe, reviewable state.
74
+ * @returns {{ ok: boolean, checks: object[] }}
75
+ */
76
+ export function checkPostconditions({ rootDir = process.cwd(), wrapupSeen = null } = {}) {
77
+ const checks = [];
78
+
79
+ // 1. digest written — the cold-review artifact must exist and be non-empty.
80
+ const digest = join(rootDir, 'OVERNIGHT.md');
81
+ const digestOk = fileExists(digest) && fileStat(digest).size > 0;
82
+ checks.push(check('digest-written', 'block', digestOk, digestOk ? 'OVERNIGHT.md present' : 'OVERNIGHT.md missing/empty — run did not produce a review'));
83
+
84
+ // 2. graceful wrap-up — a missing wrap-up means the loop may have crashed.
85
+ if (wrapupSeen !== null) {
86
+ checks.push(check('graceful-wrapup', 'block', wrapupSeen === true, wrapupSeen ? 'overnight_wrapup event recorded' : 'NO overnight_wrapup — run may have crashed mid-step'));
87
+ }
88
+
89
+ // 3. local-only — commits exist but nothing pushed beyond upstream (best-effort).
90
+ const ahead = git('rev-list --count @{upstream}..HEAD', rootDir);
91
+ if (ahead !== null) {
92
+ checks.push(check('local-commits', 'warn', true, `${ahead} local commit(s) ahead of upstream — push is the human's job`));
93
+ }
94
+
95
+ const ok = checks.filter((c) => c.severity === 'block').every((c) => c.ok);
96
+ return { ok, checks };
97
+ }
@@ -61,6 +61,15 @@ export function buildConfig({ projectName, language, depth, roles }) {
61
61
  },
62
62
  workflow: {
63
63
  default_depth: depth,
64
+ // Trust Mode (ADR-0022) — opt-in gate auto-approval. Off by default;
65
+ // present so the shape is discoverable. Guards + Critical findings are
66
+ // never relaxed regardless of these flags.
67
+ trust: {
68
+ enabled: false,
69
+ auto_approve: { scope: false, plan: false, changes: false },
70
+ max_auto_subtasks: 0,
71
+ require_evidence: false,
72
+ },
64
73
  },
65
74
  team: {
66
75
  roles: roles,
@@ -103,6 +112,53 @@ export function getToolkitVersions(config) {
103
112
  };
104
113
  }
105
114
 
115
+ // Named trust profiles (ADR-0023 follow-up) — a profile is a bundle of defaults
116
+ // answering "what does this run's trust posture look like?" Explicit keys still
117
+ // override the profile, so it's purely a convenience layer.
118
+ export const TRUST_PROFILES = {
119
+ // Vibe-coder overnight default: trust all convenience gates, bounded, and
120
+ // require evidence on GATE D so the unattended changes-approval is backed by
121
+ // a real test/review basis (safe path = easy path for the overnight audience).
122
+ 'solo-night': { enabled: true, auto_approve: { scope: true, plan: true, changes: true }, max_auto_subtasks: 8, require_evidence: true },
123
+ // Team velocity without waiving diff review: plan auto, every diff still human.
124
+ 'reviewed-team': { enabled: true, auto_approve: { scope: true, plan: true, changes: false }, max_auto_subtasks: 4 },
125
+ };
126
+
127
+ /**
128
+ * Trust Mode config for `dw:flow` gate auto-approval (ADR-0022).
129
+ * Off by default and back-compat: absent keys → fully manual gates, i.e.
130
+ * byte-for-byte today's behaviour. A `profile` supplies defaults that explicit
131
+ * keys override. Falls back to safe defaults so older config keeps working.
132
+ */
133
+ export function getTrustConfig(config) {
134
+ const t = config?.workflow?.trust || {};
135
+ const profile = t.profile && TRUST_PROFILES[t.profile] ? t.profile : null;
136
+ const base = profile ? TRUST_PROFILES[profile] : { enabled: false, auto_approve: {}, max_auto_subtasks: 0 };
137
+ const a = t.auto_approve || {};
138
+ const baseA = base.auto_approve || {};
139
+ // explicit `enabled` wins; else the profile's; else off.
140
+ const enabled = t.enabled !== undefined ? t.enabled === true : base.enabled === true;
141
+ // explicit gate key wins; else the profile's; else off. Honoured only when enabled.
142
+ const gate = (key) => enabled && (a[key] !== undefined ? a[key] === true : baseA[key] === true);
143
+ return {
144
+ enabled,
145
+ profile,
146
+ auto_approve: {
147
+ scope: gate('scope'), // GATE A / Q1
148
+ plan: gate('plan'), // GATE C
149
+ changes: gate('changes'), // GATE D (never when Critical)
150
+ },
151
+ // 0 = unbounded once enabled; >0 caps an unattended run's length.
152
+ max_auto_subtasks: Number.isInteger(t.max_auto_subtasks) && t.max_auto_subtasks > 0
153
+ ? t.max_auto_subtasks
154
+ : (base.max_auto_subtasks || 0),
155
+ // When true, GATE D refuses to auto-approve without an --evidence object —
156
+ // makes the safe (evidence-backed) outcome the default for `changes`.
157
+ // Explicit key wins; else the profile's default; else off.
158
+ require_evidence: t.require_evidence !== undefined ? t.require_evidence === true : base.require_evidence === true,
159
+ };
160
+ }
161
+
106
162
  /**
107
163
  * Renderer config for /dw:review --visual + `dw review render` (ADR-0007).
108
164
  * Falls back to defaults when keys are absent so existing projects work
@@ -205,6 +205,34 @@ export const EVENT_CATALOGUE = {
205
205
  required: { goal_id: 'string', task_id: 'string' },
206
206
  optional: {},
207
207
  },
208
+
209
+ // §5.6 Trust Mode & Overnight Autopilot (ADR-0022/0023) — written to
210
+ // events-global.jsonl; consumed by the morning digest (overnight-digest@v1).
211
+ gate_auto_approved: {
212
+ category: 'trust-autopilot',
213
+ required: { gate: 'string', actor: 'string' },
214
+ optional: { task: 'any', reason: 'any', evidence: 'any' },
215
+ },
216
+ subtask_completed: {
217
+ category: 'trust-autopilot',
218
+ required: { subtask: 'string' },
219
+ optional: { commit: 'string', summary: 'string' },
220
+ },
221
+ parked: {
222
+ category: 'trust-autopilot',
223
+ required: { item: 'string', question: 'string' },
224
+ optional: { options: 'string[]' },
225
+ },
226
+ hard_stop: {
227
+ category: 'trust-autopilot',
228
+ required: { reason: 'string' },
229
+ optional: { gate: 'string', blocker: 'string' },
230
+ },
231
+ overnight_wrapup: {
232
+ category: 'trust-autopilot',
233
+ required: { stop_reason: 'string' },
234
+ optional: {},
235
+ },
208
236
  };
209
237
 
210
238
  export const KNOWN_EVENTS = Object.freeze(Object.keys(EVENT_CATALOGUE));