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.
- package/.claude/rules/dw.md +14 -3
- package/.claude/skills/dw-flow/SKILL.md +35 -2
- package/.claude/skills/dw-goal/SKILL.md +22 -1
- package/.dw/config/config.schema.json +24 -0
- package/.dw/config/dw.config.yml +14 -0
- package/.dw/core/HARNESS.md +157 -0
- package/.dw/core/schemas/events/gate_auto_approved.schema.json +36 -0
- package/.dw/core/schemas/events/hard_stop.schema.json +35 -0
- package/.dw/core/schemas/events/index.json +28 -2
- package/.dw/core/schemas/events/overnight_wrapup.schema.json +29 -0
- package/.dw/core/schemas/events/parked.schema.json +39 -0
- package/.dw/core/schemas/events/subtask_completed.schema.json +35 -0
- package/.dw/core/schemas/overnight-digest.schema.json +113 -0
- package/.dw/core/templates/autopilot-cron.sh +28 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/cli.mjs +63 -0
- package/src/commands/autopilot.mjs +65 -0
- package/src/commands/doctor.mjs +17 -1
- package/src/commands/overnight.mjs +35 -0
- package/src/commands/trust.mjs +79 -0
- package/src/lib/autopilot-contract.mjs +97 -0
- package/src/lib/config.mjs +56 -0
- package/src/lib/event-schema.mjs +28 -0
- package/src/lib/goal-events.mjs +4 -1
- package/src/lib/overnight-digest.mjs +241 -0
- package/src/lib/tls-helpers.mjs +28 -6
- package/src/lib/trust.mjs +139 -0
- package/README.vi.md +0 -230
|
@@ -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
|
|
3
|
+
🇬🇧 **English** · [🇻🇳 Tiếng Việt](README-vi.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dw-kit)
|
|
6
6
|
[](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
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
|
+
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -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
|
package/src/lib/event-schema.mjs
CHANGED
|
@@ -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));
|