dw-kit 1.9.2 → 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 +152 -121
- package/package.json +4 -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/commands/voice.mjs +431 -2
- package/src/lib/autopilot-contract.mjs +97 -0
- package/src/lib/board-data.mjs +23 -57
- package/src/lib/config.mjs +56 -0
- package/src/lib/event-schema.mjs +28 -0
- package/src/lib/goal-driver.mjs +312 -0
- package/src/lib/goal-events.mjs +4 -1
- package/src/lib/goal-progress.mjs +193 -0
- package/src/lib/overnight-digest.mjs +241 -0
- package/src/lib/process-kill.mjs +77 -0
- package/src/lib/task-md-utils.mjs +78 -0
- package/src/lib/tls-helpers.mjs +28 -6
- package/src/lib/trust.mjs +139 -0
|
@@ -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
|
+
}
|