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.
Files changed (34) hide show
  1. package/.claude/rules/dw.md +14 -3
  2. package/.claude/skills/dw-flow/SKILL.md +35 -2
  3. package/.claude/skills/dw-goal/SKILL.md +22 -1
  4. package/.dw/config/config.schema.json +24 -0
  5. package/.dw/config/dw.config.yml +14 -0
  6. package/.dw/core/HARNESS.md +157 -0
  7. package/.dw/core/schemas/events/gate_auto_approved.schema.json +36 -0
  8. package/.dw/core/schemas/events/hard_stop.schema.json +35 -0
  9. package/.dw/core/schemas/events/index.json +28 -2
  10. package/.dw/core/schemas/events/overnight_wrapup.schema.json +29 -0
  11. package/.dw/core/schemas/events/parked.schema.json +39 -0
  12. package/.dw/core/schemas/events/subtask_completed.schema.json +35 -0
  13. package/.dw/core/schemas/overnight-digest.schema.json +113 -0
  14. package/.dw/core/templates/autopilot-cron.sh +28 -0
  15. package/README.md +152 -121
  16. package/package.json +4 -1
  17. package/src/cli.mjs +63 -0
  18. package/src/commands/autopilot.mjs +65 -0
  19. package/src/commands/doctor.mjs +17 -1
  20. package/src/commands/overnight.mjs +35 -0
  21. package/src/commands/trust.mjs +79 -0
  22. package/src/commands/voice.mjs +431 -2
  23. package/src/lib/autopilot-contract.mjs +97 -0
  24. package/src/lib/board-data.mjs +23 -57
  25. package/src/lib/config.mjs +56 -0
  26. package/src/lib/event-schema.mjs +28 -0
  27. package/src/lib/goal-driver.mjs +312 -0
  28. package/src/lib/goal-events.mjs +4 -1
  29. package/src/lib/goal-progress.mjs +193 -0
  30. package/src/lib/overnight-digest.mjs +241 -0
  31. package/src/lib/process-kill.mjs +77 -0
  32. package/src/lib/task-md-utils.mjs +78 -0
  33. package/src/lib/tls-helpers.mjs +28 -6
  34. package/src/lib/trust.mjs +139 -0
@@ -0,0 +1,139 @@
1
+ // Trust Mode gate-resolution logic (ADR-0022).
2
+ //
3
+ // `dw:flow` human gates fall into two classes. Trust Mode may auto-approve the
4
+ // *convenience* gates (reversible review of intent/output) when the user opts
5
+ // in; it can NEVER relax the *safety* gates (irreversible / unsafe). This module
6
+ // is the single source of truth for that taxonomy, so the skill, the CLI, and
7
+ // the tests all agree on what is and isn't relaxable.
8
+
9
+ import { getTrustConfig } from './config.mjs';
10
+ import { logGoalEvent } from './goal-events.mjs';
11
+
12
+ // Canonical gate ids and their class. Gate ids are the stable contract the
13
+ // skill passes in (mapped from GATE A/B/C/D and the quick-depth Q1/Q2).
14
+ export const GATES = {
15
+ scope: { class: 'convenience', maps: 'GATE A / Q1', configKey: 'scope' },
16
+ plan: { class: 'convenience', maps: 'GATE C', configKey: 'plan' },
17
+ changes: { class: 'convenience', maps: 'GATE D / Q2', configKey: 'changes' },
18
+ arch: { class: 'safety', maps: 'GATE B (TL architecture sign-off)', configKey: null },
19
+ };
20
+
21
+ /**
22
+ * Normalize a one-shot `--trust` flag into a predicate over gate ids.
23
+ * Accepts:
24
+ * true → trust every convenience gate this run
25
+ * "plan" → trust only that gate
26
+ * "scope,plan" → trust those gates
27
+ * ["scope","plan"]→ same
28
+ * undefined/false → no one-shot trust
29
+ */
30
+ export function parseTrustFlag(flag) {
31
+ if (flag === true) return { all: true, gates: new Set() };
32
+ if (!flag) return { all: false, gates: new Set() };
33
+ // CLI passes strings; treat "true"/"all" (the bare `--trust`) as "every convenience gate".
34
+ const raw = Array.isArray(flag) ? flag : String(flag).split(',');
35
+ const gates = new Set(raw.map((g) => g.trim().toLowerCase()).filter(Boolean));
36
+ if (gates.has('true') || gates.has('all')) return { all: true, gates: new Set() };
37
+ return { all: false, gates };
38
+ }
39
+
40
+ /**
41
+ * Decide whether a single gate may be auto-approved.
42
+ *
43
+ * @param {object} params
44
+ * @param {string} params.gate canonical gate id (see GATES)
45
+ * @param {object} params.config loaded dw config (for workflow.trust)
46
+ * @param {*} [params.flag] one-shot `--trust` flag value (this run)
47
+ * @param {boolean}[params.hasCritical] true when a Critical review finding exists (GATE D)
48
+ * @param {object} [params.evidence] EVD verify (ADR-0023): {tests:{exit},review:{critical}} — when
49
+ * present, criticality/red-tests are derived from the evidence
50
+ * itself, not just the caller's flag.
51
+ * @returns {{ autoApprove: boolean, reason: string, gateClass: string }}
52
+ */
53
+ export function resolveGate({ gate, config, flag, hasCritical = false, evidence, runCount } = {}) {
54
+ const meta = GATES[gate];
55
+ if (!meta) {
56
+ return { autoApprove: false, reason: `unknown gate "${gate}" — manual`, gateClass: 'unknown' };
57
+ }
58
+
59
+ // Invariant: safety hard-stops are structurally unreachable by Trust Mode.
60
+ if (meta.class === 'safety') {
61
+ return {
62
+ autoApprove: false,
63
+ reason: `safety gate (${meta.maps}) — never auto-approved`,
64
+ gateClass: 'safety',
65
+ };
66
+ }
67
+
68
+ // EVD verify (ADR-0023): when evidence is supplied for GATE D, derive the
69
+ // safety signals from the evidence — stronger than a bare --critical flag the
70
+ // caller might forget. Evidence that shows a Critical finding or red tests
71
+ // forces a manual stop even when `changes` is trusted.
72
+ const criticalFromEvidence = gate === 'changes' && evidence?.review && Number(evidence.review.critical) >= 1;
73
+ // Only treat tests as failed when a real exit code is present and non-zero.
74
+ // A `tests` object without `exit` must NOT block (round-2 verify: false positive).
75
+ const exitCode = Number(evidence?.tests?.exit);
76
+ const testsFailedFromEvidence = gate === 'changes' && evidence?.tests && Number.isFinite(exitCode) && exitCode !== 0;
77
+
78
+ // Invariant: a Critical review finding forces GATE D to a manual stop even
79
+ // when `changes` is trusted.
80
+ if (gate === 'changes' && (hasCritical || criticalFromEvidence)) {
81
+ return {
82
+ autoApprove: false,
83
+ reason: 'Critical review finding — manual approval required',
84
+ gateClass: 'convenience',
85
+ };
86
+ }
87
+ if (testsFailedFromEvidence) {
88
+ return {
89
+ autoApprove: false,
90
+ reason: `tests not green (exit ${evidence.tests.exit}) — manual approval required`,
91
+ gateClass: 'convenience',
92
+ };
93
+ }
94
+
95
+ const trust = getTrustConfig(config);
96
+
97
+ // require_evidence (ADR-0023 follow-up): when set, GATE D will not auto-approve
98
+ // without an evidence object citing a REAL numeric review.critical count — an
99
+ // empty {review:{}} must NOT satisfy it (round-3 verify: bypass via empty object).
100
+ // Require a real numeric critical count — a boolean `false` or `[]` both coerce
101
+ // to 0 via Number(), so check the type, not just finiteness (final verify).
102
+ const hasReviewCount = typeof evidence?.review?.critical === 'number'
103
+ && Number.isFinite(evidence.review.critical);
104
+ if (gate === 'changes' && trust.require_evidence && !hasReviewCount) {
105
+ return {
106
+ autoApprove: false,
107
+ reason: 'workflow.trust.require_evidence — supply --evidence with a numeric review.critical count',
108
+ gateClass: 'convenience',
109
+ };
110
+ }
111
+
112
+ const oneShot = parseTrustFlag(flag);
113
+ if (oneShot.all || oneShot.gates.has(gate)) {
114
+ return { autoApprove: true, reason: `--trust (this run): ${meta.maps}`, gateClass: 'convenience' };
115
+ }
116
+
117
+ if (trust.enabled && trust.auto_approve[meta.configKey]) {
118
+ // Budget: once an unattended run reaches its cap, stop auto-approving so the
119
+ // bound is real at the decision point, not just advisory (round-2 verify: W-3).
120
+ if (trust.max_auto_subtasks > 0 && Number.isInteger(runCount) && runCount >= trust.max_auto_subtasks) {
121
+ return { autoApprove: false, reason: `budget reached (${runCount}/${trust.max_auto_subtasks} auto-approved) — manual`, gateClass: 'convenience' };
122
+ }
123
+ return { autoApprove: true, reason: `workflow.trust.auto_approve.${meta.configKey}`, gateClass: 'convenience' };
124
+ }
125
+
126
+ // Invariant: default is fully manual.
127
+ return { autoApprove: false, reason: 'manual (Trust Mode off for this gate)', gateClass: 'convenience' };
128
+ }
129
+
130
+ /**
131
+ * Audit an auto-approval to .dw/events-global.jsonl so the skipped human is
132
+ * always recoverable. Invariant 4 of ADR-0022.
133
+ */
134
+ export function logTrustEvent({ gate, task, reason, evidence }, rootDir = process.cwd()) {
135
+ return logGoalEvent(
136
+ { event: 'gate_auto_approved', actor: 'trust-mode', gate, task: task || null, reason: reason || null, evidence: evidence || null },
137
+ rootDir,
138
+ );
139
+ }