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
package/src/lib/goal-events.mjs
CHANGED
|
@@ -43,7 +43,10 @@ function rotateIfNeeded(file) {
|
|
|
43
43
|
|
|
44
44
|
// Issue #14 Bug 1 — same smart truncation as agent-events.mjs adapted for goal events.
|
|
45
45
|
// Identifying fields differ: goal_id + field instead of claim_id + agent_id.
|
|
46
|
-
|
|
46
|
+
// Includes trust/autopilot audit fields (gate, actor, reason) so a
|
|
47
|
+
// gate_auto_approved event stays attributable even under hard truncation
|
|
48
|
+
// (review round 2: W-5).
|
|
49
|
+
const GOAL_IDENTIFY_FIELDS = ['ts', 'event', 'goal_id', 'kr_id', 'field', 'changed_by', 'gate', 'actor', 'reason', 'task'];
|
|
47
50
|
|
|
48
51
|
function smartTruncateGoal(enriched, originalBytes) {
|
|
49
52
|
// Step 1: truncate large string-valued fields like `new` (long summaries) or `staged_files` arrays
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Morning-review digest builder for Overnight Autopilot (ADR-0023).
|
|
2
|
+
//
|
|
3
|
+
// An unattended overnight run emits events to .dw/events-global.jsonl; this
|
|
4
|
+
// module turns a run's events into ONE scannable artifact a human reads cold
|
|
5
|
+
// the next morning. Pure functions (buildDigest/summarizeRun) so the output is
|
|
6
|
+
// deterministic and unit-testable; writeDigest handles the filesystem.
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const DIGEST_SCHEMA_VERSION = 'overnight-digest@v1';
|
|
12
|
+
export const DIGEST_FILE = 'OVERNIGHT.md';
|
|
13
|
+
|
|
14
|
+
// Event types the autopilot loop emits (subset of events-global.jsonl).
|
|
15
|
+
const EV_DONE = 'subtask_completed';
|
|
16
|
+
const EV_AUTO = 'gate_auto_approved';
|
|
17
|
+
const EV_PARK = 'parked';
|
|
18
|
+
const EV_STOP = 'hard_stop';
|
|
19
|
+
const EV_WRAP = 'overnight_wrapup';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reduce a run's events into counts + grouped items.
|
|
23
|
+
* @param {object[]} events
|
|
24
|
+
*/
|
|
25
|
+
export function summarizeRun(events = []) {
|
|
26
|
+
const done = [];
|
|
27
|
+
const autoApproved = [];
|
|
28
|
+
const parked = [];
|
|
29
|
+
const hardStops = [];
|
|
30
|
+
let stopReason = 'unknown';
|
|
31
|
+
let wrapupSeen = false;
|
|
32
|
+
|
|
33
|
+
for (const e of events) {
|
|
34
|
+
switch (e && e.event) {
|
|
35
|
+
case EV_DONE: done.push(e); break;
|
|
36
|
+
case EV_AUTO: autoApproved.push(e); break;
|
|
37
|
+
case EV_PARK: parked.push(e); break;
|
|
38
|
+
case EV_STOP: hardStops.push(e); break;
|
|
39
|
+
case EV_WRAP: wrapupSeen = true; if (e.stop_reason) stopReason = e.stop_reason; break;
|
|
40
|
+
default: break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
counts: {
|
|
46
|
+
done: done.length,
|
|
47
|
+
parked: parked.length,
|
|
48
|
+
hardStops: hardStops.length,
|
|
49
|
+
autoApproved: autoApproved.length,
|
|
50
|
+
},
|
|
51
|
+
stopReason,
|
|
52
|
+
wrapupSeen,
|
|
53
|
+
done,
|
|
54
|
+
autoApproved,
|
|
55
|
+
parked,
|
|
56
|
+
hardStops,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The machine-readable read-contract object (overnight-digest@v1). OVERNIGHT.md
|
|
62
|
+
* is for humans; this is what the schema describes and adapters consume.
|
|
63
|
+
*/
|
|
64
|
+
// Coerce a possibly-missing/odd value to a string or undefined — so projected
|
|
65
|
+
// items always satisfy the overnight-digest@v1 schema regardless of raw event
|
|
66
|
+
// shape (round-3 verify: malformed events must not produce schema-invalid JSON).
|
|
67
|
+
const str = (v) => (v == null ? undefined : String(v));
|
|
68
|
+
const strArr = (v) => (Array.isArray(v) ? v.map(String) : undefined);
|
|
69
|
+
const int = (v) => { const n = Math.trunc(Number(v)); return Number.isFinite(n) ? n : undefined; };
|
|
70
|
+
const nnInt = (v) => { const n = int(v); return n != null && n >= 0 ? n : undefined; }; // schema minimum:0
|
|
71
|
+
const bool = (v) => (typeof v === 'boolean' ? v : undefined);
|
|
72
|
+
const prune = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
73
|
+
|
|
74
|
+
// Project an evidence object to the schema-typed shape so OVERNIGHT.json stays
|
|
75
|
+
// valid even if an event stored loose types (e.g. tests.exit as a string).
|
|
76
|
+
function projectEvidence(ev) {
|
|
77
|
+
if (!ev || typeof ev !== 'object') return undefined;
|
|
78
|
+
const out = {};
|
|
79
|
+
if (ev.tests && typeof ev.tests === 'object') {
|
|
80
|
+
out.tests = prune({ command: str(ev.tests.command), passed: int(ev.tests.passed), failed: int(ev.tests.failed), exit: int(ev.tests.exit) });
|
|
81
|
+
}
|
|
82
|
+
if (ev.review && typeof ev.review === 'object') {
|
|
83
|
+
out.review = prune({ critical: nnInt(ev.review.critical), warning: nnInt(ev.review.warning), suggestion: nnInt(ev.review.suggestion) });
|
|
84
|
+
}
|
|
85
|
+
if (ev.diff && typeof ev.diff === 'object') {
|
|
86
|
+
out.diff = prune({ files: int(ev.diff.files), insertions: int(ev.diff.insertions), deletions: int(ev.diff.deletions), within_scope: bool(ev.diff.within_scope) });
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(out).length ? out : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildDigestData({ events = [], date = new Date().toISOString().slice(0, 10), goal = null } = {}) {
|
|
92
|
+
const s = summarizeRun(events);
|
|
93
|
+
return {
|
|
94
|
+
schema_version: DIGEST_SCHEMA_VERSION,
|
|
95
|
+
date,
|
|
96
|
+
goal,
|
|
97
|
+
counts: s.counts,
|
|
98
|
+
stopReason: s.stopReason,
|
|
99
|
+
wrapupSeen: s.wrapupSeen,
|
|
100
|
+
done: s.done.map((e) => prune({ subtask: str(e.subtask), commit: str(e.commit), summary: str(e.summary) })),
|
|
101
|
+
autoApproved: s.autoApproved.map((e) => prune({ gate: str(e.gate), task: e.task == null ? null : String(e.task), reason: str(e.reason), evidence: projectEvidence(e.evidence) })),
|
|
102
|
+
parked: s.parked.map((e) => prune({ item: str(e.item), question: str(e.question), options: strArr(e.options) })),
|
|
103
|
+
hardStops: s.hardStops.map((e) => prune({ gate: str(e.gate), blocker: str(e.blocker), reason: str(e.reason) })),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function line(...parts) { return parts.join(' '); }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render the morning-review digest as Markdown.
|
|
111
|
+
* @param {object} params
|
|
112
|
+
* @param {object[]} params.events run events
|
|
113
|
+
* @param {string} [params.date] ISO date string for the header
|
|
114
|
+
* @param {string} [params.goal] goal id/title the run pursued
|
|
115
|
+
*/
|
|
116
|
+
export function buildDigest({ events = [], date = new Date().toISOString().slice(0, 10), goal = null } = {}) {
|
|
117
|
+
const s = summarizeRun(events);
|
|
118
|
+
const out = [];
|
|
119
|
+
|
|
120
|
+
out.push(`# Overnight Autopilot — ${date}`);
|
|
121
|
+
out.push('');
|
|
122
|
+
out.push(`> ${DIGEST_SCHEMA_VERSION} · auto-generated from \`.dw/events-global.jsonl\`. Read cold, act on the **⏸ Parked** section, then review the diff.`);
|
|
123
|
+
if (goal) out.push(`> Goal: ${goal}`);
|
|
124
|
+
out.push('');
|
|
125
|
+
|
|
126
|
+
// 1. Verdict line
|
|
127
|
+
out.push('## Verdict');
|
|
128
|
+
out.push(line(
|
|
129
|
+
`✅ ${s.counts.done} done`,
|
|
130
|
+
`· ⏸ ${s.counts.parked} parked`,
|
|
131
|
+
`· 🛑 ${s.counts.hardStops} hard-stops`,
|
|
132
|
+
`· ⚡ ${s.counts.autoApproved} auto-approved`,
|
|
133
|
+
`· ⌛ stopped: ${s.stopReason}`,
|
|
134
|
+
));
|
|
135
|
+
// No wrap-up event despite work having happened = the loop may have died
|
|
136
|
+
// mid-run without the graceful stop ADR-0023 promises. Surface that loudly.
|
|
137
|
+
// Zero-activity (no events at all) is "no run", not a crash — don't warn
|
|
138
|
+
// (round-2 verify: 5.4 false-positive fix).
|
|
139
|
+
const hadActivity = s.counts.done + s.counts.parked + s.counts.hardStops + s.counts.autoApproved > 0;
|
|
140
|
+
if (!s.wrapupSeen && hadActivity) {
|
|
141
|
+
out.push('');
|
|
142
|
+
out.push('> 🛑 **NO WRAP-UP event found — the run may have crashed.** Inspect the working tree and tests manually before resuming; an irreversible step may have been left half-done.');
|
|
143
|
+
}
|
|
144
|
+
out.push('');
|
|
145
|
+
|
|
146
|
+
// 2. Done
|
|
147
|
+
out.push('## ✅ Done');
|
|
148
|
+
if (s.done.length === 0) {
|
|
149
|
+
out.push('_Nothing completed this run._');
|
|
150
|
+
} else {
|
|
151
|
+
for (const e of s.done) {
|
|
152
|
+
const sha = e.commit ? ` \`${String(e.commit).slice(0, 9)}\`` : '';
|
|
153
|
+
out.push(`- ${e.subtask || e.summary || 'subtask'}${sha}${e.summary && e.subtask ? ` — ${e.summary}` : ''}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
out.push('');
|
|
157
|
+
|
|
158
|
+
// 3. Auto-approved
|
|
159
|
+
out.push('## ⚡ Auto-approved (Trust Mode)');
|
|
160
|
+
if (s.autoApproved.length === 0) {
|
|
161
|
+
out.push('_No gates were auto-approved._');
|
|
162
|
+
} else {
|
|
163
|
+
for (const e of s.autoApproved) {
|
|
164
|
+
out.push(`- **${e.gate || 'gate'}**${e.task ? ` (${e.task})` : ''} — ${e.reason || 'trusted'}`);
|
|
165
|
+
// EVD verify (ADR-0023 §evidence): show the BASIS, not just the outcome,
|
|
166
|
+
// so a human can spot a call they'd have made differently.
|
|
167
|
+
const ev = e.evidence;
|
|
168
|
+
if (ev && typeof ev === 'object') {
|
|
169
|
+
const bits = [];
|
|
170
|
+
if (ev.tests) bits.push(`tests ${ev.tests.passed ?? '?'}/${ev.tests.failed ?? '?'} ${Number(ev.tests.exit) === 0 ? '✓' : '✗'}`);
|
|
171
|
+
if (ev.review) bits.push(`${ev.review.critical ?? '?'} Critical · ${ev.review.warning ?? 0} Warning`);
|
|
172
|
+
if (ev.diff) bits.push(`diff +${ev.diff.insertions ?? '?'}/−${ev.diff.deletions ?? '?'} in ${ev.diff.files ?? '?'} files${ev.diff.within_scope === false ? ' ⚠ OUT OF SCOPE' : ''}`);
|
|
173
|
+
if (bits.length) out.push(` basis: ${bits.join(' · ')}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
out.push('');
|
|
178
|
+
|
|
179
|
+
// 4. Parked for you — the section the human acts on
|
|
180
|
+
out.push('## ⏸ Parked for you');
|
|
181
|
+
if (s.parked.length === 0) {
|
|
182
|
+
out.push('_Nothing parked — the agent did not hit an ambiguous or irreversible decision._');
|
|
183
|
+
} else {
|
|
184
|
+
for (const e of s.parked) {
|
|
185
|
+
out.push(`- **${e.item || 'decision'}** — ${e.question || 'needs your call'}`);
|
|
186
|
+
if (Array.isArray(e.options) && e.options.length) {
|
|
187
|
+
out.push(` - options: ${e.options.join(' · ')}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
out.push('');
|
|
192
|
+
|
|
193
|
+
// 5. Hard-stops hit
|
|
194
|
+
out.push('## 🛑 Hard-stops hit');
|
|
195
|
+
if (s.hardStops.length === 0) {
|
|
196
|
+
out.push('_No Guard or Critical hard-stop fired._');
|
|
197
|
+
} else {
|
|
198
|
+
for (const e of s.hardStops) {
|
|
199
|
+
out.push(`- **${e.gate || e.blocker || 'hard-stop'}** — ${e.reason || e.blocker || 'blocked'}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
out.push('');
|
|
203
|
+
|
|
204
|
+
// 6. Review next
|
|
205
|
+
out.push('## Review next');
|
|
206
|
+
out.push('```bash');
|
|
207
|
+
out.push('git log --oneline --since="12 hours ago" # what landed overnight');
|
|
208
|
+
out.push('git diff main...HEAD # full diff to review');
|
|
209
|
+
out.push('dw goal status # outcome progress');
|
|
210
|
+
out.push('# resume the loop after you clear parked items:');
|
|
211
|
+
out.push('dw goal --auto=full --trust');
|
|
212
|
+
out.push('```');
|
|
213
|
+
out.push('');
|
|
214
|
+
|
|
215
|
+
return out.join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Write the digest to OVERNIGHT.md, rotating any prior run into
|
|
220
|
+
* .dw/cache/overnight/{date}.md so history is kept but the root stays a single
|
|
221
|
+
* current file.
|
|
222
|
+
*/
|
|
223
|
+
export function writeDigest(rootDir, content, { date = new Date().toISOString().slice(0, 10) } = {}) {
|
|
224
|
+
const target = join(rootDir, DIGEST_FILE);
|
|
225
|
+
if (existsSync(target)) {
|
|
226
|
+
const archiveDir = join(rootDir, '.dw', 'cache', 'overnight');
|
|
227
|
+
if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
|
|
228
|
+
// Avoid clobbering a prior same-day run's archive (review round 2: W-1).
|
|
229
|
+
let archive = join(archiveDir, `${date}.md`);
|
|
230
|
+
for (let n = 1; existsSync(archive); n++) archive = join(archiveDir, `${date}-${n}.md`);
|
|
231
|
+
try {
|
|
232
|
+
renameSync(target, archive);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
try { process.stderr.write(`⚠ overnight: could not rotate prior ${DIGEST_FILE} (${e.message}) — overwriting.\n`); } catch { /* stderr closed */ }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const dir = dirname(target);
|
|
238
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
239
|
+
writeFileSync(target, content, 'utf8');
|
|
240
|
+
return target;
|
|
241
|
+
}
|
package/src/lib/tls-helpers.mjs
CHANGED
|
@@ -10,12 +10,33 @@
|
|
|
10
10
|
//
|
|
11
11
|
// F-24 fix (G-rgoal-realtime-orch KR-A production-readiness).
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from 'node:fs';
|
|
14
|
+
import { join, delimiter } from 'node:path';
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
16
|
import { networkInterfaces } from 'node:os';
|
|
17
17
|
import { resolveCommandPath } from './spawn-helpers.mjs';
|
|
18
18
|
|
|
19
|
+
// Whether `cmd` is actually an executable on PATH.
|
|
20
|
+
//
|
|
21
|
+
// `resolveCommandPath` is spawn-oriented: on POSIX it returns `found: true`
|
|
22
|
+
// unconditionally (spawn resolves PATH itself), so it CANNOT answer "is this
|
|
23
|
+
// binary installed?" — trusting its `.found` here false-positived `mkcert` on
|
|
24
|
+
// any Linux box without it. So on POSIX we walk PATH and check for an
|
|
25
|
+
// executable regular file; on Win32 `resolveCommandPath` already does a real
|
|
26
|
+
// PATH+PATHEXT walk, so we defer to it.
|
|
27
|
+
function commandInstalled(cmd) {
|
|
28
|
+
if (process.platform === 'win32') return resolveCommandPath(cmd).found;
|
|
29
|
+
const dirs = (process.env.PATH || '').split(delimiter).filter(Boolean);
|
|
30
|
+
for (const dir of dirs) {
|
|
31
|
+
const p = join(dir, cmd);
|
|
32
|
+
try {
|
|
33
|
+
const st = statSync(p);
|
|
34
|
+
if (st.isFile() && (st.mode & 0o111) !== 0) return true; // any exec bit
|
|
35
|
+
} catch { /* not here — keep looking */ }
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const TLS_DIR_REL = '.dw/cache/voice/tls';
|
|
20
41
|
const KEY_FILE = 'key.pem';
|
|
21
42
|
const CERT_FILE = 'cert.pem';
|
|
@@ -29,10 +50,11 @@ const CERT_DAYS = 3650; // 10y — local cert; we don't rotate often
|
|
|
29
50
|
* Returns { name: 'mkcert' | 'openssl' | 'none', path: string | null }
|
|
30
51
|
*/
|
|
31
52
|
export function detectTlsProvider() {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
for (const name of ['mkcert', 'openssl']) {
|
|
54
|
+
if (!commandInstalled(name)) continue;
|
|
55
|
+
const r = resolveCommandPath(name); // path + needsShell details
|
|
56
|
+
return { name, path: r.path, needsShell: r.needsShell };
|
|
57
|
+
}
|
|
36
58
|
return { name: 'none', path: null, needsShell: false };
|
|
37
59
|
}
|
|
38
60
|
|
|
@@ -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
|
+
}
|
package/README.vi.md
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
# dw-kit
|
|
2
|
-
|
|
3
|
-
[🇬🇧 English](README.md) · 🇻🇳 **Tiếng Việt**
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/dw-kit)
|
|
6
|
-
[](https://www.npmjs.com/package/dw-kit)
|
|
7
|
-
[](https://nodejs.org)
|
|
8
|
-
[](LICENSE)
|
|
9
|
-
|
|
10
|
-
> **Lớp harness quản trị cho việc dev cùng AI.** Cho con agent code của bạn một quy trình lặp lại được, hàng rào an toàn, và một bộ nhớ sống lâu hơn cửa sổ chat — để nó giao việc đã sẵn-sàng-review thay vì đoán mò đầy tự tin.
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install -g dw-kit && dw init
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Sau đó, ngay trong AI IDE của bạn:
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
/dw:flow làm cái việc bạn thực sự muốn
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Xong. `dw` lo phần còn lại — và **dừng lại chờ bạn duyệt** trước khi viết code.
|
|
23
|
-
|
|
24
|
-
> **Dùng được với bất kỳ AI agent có CLI nào** — Claude Code, Codex, Gemini, Cursor, Windsurf, Copilot. Bản thân CLI `dw` và bộ methodology bằng Markdown là universal: agent của bạn đọc được file và chạy được shell là dùng được dw-kit. Bộ harness *đầy đủ* (hooks, tự động hóa skill, điều phối đa-agent) ở mức hạng-nhất trên **[Claude Code](https://claude.ai/code)**; các agent/IDE còn lại nhận lớp methodology qua generic adapter.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Vì sao cần dw-kit?
|
|
29
|
-
|
|
30
|
-
AI agent nhanh nhưng không nhớ gì: quên ngữ cảnh giữa các session, bỏ qua bước lập kế hoạch, và chẳng có khái niệm "khoan đã, để con người ngó qua cái này trước." dw-kit bọc agent của bạn trong một quy trình gọn nhẹ — giữ lại tốc độ, bỏ đi hỗn loạn.
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
Khởi tạo → Khảo sát → Lập KH → Thực thi → Kiểm chứng → Đóng
|
|
34
|
-
│ │ │ │ │ │
|
|
35
|
-
scope đọc thiết kế TDD, quality handoff
|
|
36
|
-
+ docs code (✋ DỪNG 1 commit gates + + lưu
|
|
37
|
-
chờ OK) /subtask review trữ
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
- **✋ Checkpoint của con người** — agent dừng ở bước Lập kế hoạch chờ bạn gật đầu, không tự ý phóng tới.
|
|
41
|
-
- **🛡️ Guards** — hook chặn commit secret, `.env`, và sửa đổi nguy hiểm ngay trước khi chúng xảy ra.
|
|
42
|
-
- **🧠 Bộ nhớ sống qua nhiều session** — quyết định, task docs, ghi chú bàn giao giúp session sau tiếp tục từ con số không.
|
|
43
|
-
- **🌏 Song ngữ trọn vẹn** — UX tiếng Anh + tiếng Việt ngay từ đầu.
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## Sinh ra để tự hiệu chỉnh — với bạn trong vòng lặp
|
|
48
|
-
|
|
49
|
-
Hai cơ chế giữ cho agent trung thực chứ không chỉ "bận rộn".
|
|
50
|
-
|
|
51
|
-
**Vòng lặp đeo bám — `/dw:goal`.** Phần lớn agent tối ưu cho *"tôi đã làm xong task."* dw-kit tối ưu cho *kết quả*. Đưa cho nó một Goal với các Key Result đo được, nó chạy một vòng lặp kiểu OODA, **đo lại khoảng cách (gap) sau mỗi vòng**. Khi một cách thất bại, nó **nâng cấp tư duy** — leo lên một bậc thang lập luận — chứ không lặp lại nước đi cũ. Nó chỉ thoát khi KR thực sự chạm target, không phải khi hết hơi. *(ADR-0016)*
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
đo gap → hành động → đo lại ──┐
|
|
55
|
-
▲ │
|
|
56
|
-
└──── kẹt? leo bậc ◀───────┘ leo thang, đừng lặp lại
|
|
57
|
-
chỉ thoát khi Key Result đạt
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Độ tự chủ là một núm vặn: **supervised** (dừng mỗi vòng) → **checkpoint** (`--auto`, dừng ở mỗi KR đạt) → **full** (`--auto=full`, chỉ hard-stop), giới hạn bởi `--max-iter`.
|
|
61
|
-
|
|
62
|
-
**Phản biện đối kháng, rồi tới bạn — `/dw:plan`.** Trước khi một plan tới tay bạn, dw "quay" nó bằng một màn **tranh luận red-team / blue-team**: red tấn công tìm failure mode và giả định ngầm, blue củng cố hoặc nhượng bộ. Bạn nhận được plan *đã được mài sắc* kèm những điểm còn tranh cãi — rồi **việc thực thi dừng lại chờ bạn duyệt.** Phản xạ ấy lặp lại ở bước **Kiểm chứng** (`/dw:review`: tính đúng · bảo mật · convention · độ phủ) và ở mọi commit gate. Agent đề xuất; bạn quyết định và lái.
|
|
63
|
-
|
|
64
|
-
> Debate tùy theo depth — tắt với `quick`, tự kích hoạt khi phát hiện tín hiệu high-stakes ở `standard`, bật mặc định với `thorough`. Override bằng `--debate` / `--no-debate`.
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## Harness: dw quản trị agent của bạn như thế nào
|
|
69
|
-
|
|
70
|
-
dw-kit nằm *bên trên* con agent code như một lớp harness quản trị mỏng — ba tầng bền vững mà nó đọc và ghi trong lúc làm việc:
|
|
71
|
-
|
|
72
|
-
| Tầng | Quản lý | Artifact |
|
|
73
|
-
|------|---------|----------|
|
|
74
|
-
| **Context** | Thứ agent biết, ở mỗi session | `CLAUDE.md`, ADR (`/dw:decision`), ghi chú handoff |
|
|
75
|
-
| **Tasks** | Một nguồn sự thật cho mỗi đơn vị công việc | `task.md` (v3 — lint + schema, chống drift trạng thái), index `ACTIVE.md` |
|
|
76
|
-
| **Goals** | Kết quả ở tầng trên task | `dw goal *`, Key Results, link task ↔ goal |
|
|
77
|
-
|
|
78
|
-
Vì trạng thái này nằm trong **file thuần, không phải cửa sổ chat**, nó sống qua reset session, đổi model, và bàn giao giữa agent với agent hoặc với người — thứ một trợ lý IDE bó hẹp trong một session về cấu trúc không thể sở hữu.
|
|
79
|
-
|
|
80
|
-
### Điều phối đa-agent song song (Agent OS)
|
|
81
|
-
|
|
82
|
-
Một task hiếm khi chỉ cần một agent. **Agent OS** cho phép một orchestrator chia `task.md` thành các đơn vị công việc có ranh giới rồi giao cho *các* agent chạy **song song** — ví dụ Claude Code lo API, Codex lo test, Gemini lo docs, một con người lo phần migration hóc búa:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
dw agent claim payments --subtasks ST-1 --write "src/api/**" --vendor claude --lease 1h
|
|
86
|
-
dw agent claim payments --subtasks ST-2 --write "test/**" --vendor codex
|
|
87
|
-
dw agent conflicts # exit 1 nếu hai claim đè nhau về file hoặc subtask
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Mỗi agent **claim** subtask + phạm vi file trước khi sửa, nên việc song song không giẫm chân nhau. Đây là cơ chế **hợp tác, không cưỡng bức**: trùng lặp bị bắt bởi `dw agent conflicts` và hook `pre-commit-gate`, report của từng agent gộp ngược về một `task.md` duy nhất, và **orchestrator dừng chờ bạn duyệt trước khi push.** *(ADR-0009)*
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Bắt đầu nhanh
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# 1. Cài đặt
|
|
98
|
-
npm install -g dw-kit
|
|
99
|
-
|
|
100
|
-
# 2. Thiết lập trong project (wizard, hoặc --solo cho zero-config)
|
|
101
|
-
dw init
|
|
102
|
-
dw init --solo # chế độ vibe-coder: chỉ guards, không task docs
|
|
103
|
-
|
|
104
|
-
# 3. Drive một task từ AI IDE
|
|
105
|
-
/dw:flow thêm tính năng đăng nhập
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Vài skill khác nên biết:
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
/dw:prompt # biến ý tưởng mơ hồ thành prompt sắc, làm được ngay
|
|
112
|
-
/dw:plan # lập kế hoạch + tự phản biện red/blue, rồi dừng chờ duyệt
|
|
113
|
-
/dw:review # tính đúng · bảo mật · convention · độ phủ test
|
|
114
|
-
/dw:decision # ghi lại một quyết định kiến trúc (ADR)
|
|
115
|
-
/dw:goal # drive một Goal tới các Key Result (vòng lặp đeo bám)
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Hướng dẫn nhanh: ship tính năng đầu tiên
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
dw init # một lần: dựng .dw/ + .claude/ trong repo của bạn
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Sau đó, ngay trong Claude Code:
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
/dw:flow thêm chức năng reset mật khẩu qua email
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Diễn biến, theo thứ tự:
|
|
133
|
-
|
|
134
|
-
1. **Khởi tạo + Khảo sát** — dw chốt phạm vi, dựng `task.md`, và đọc code auth của bạn (chưa sửa gì).
|
|
135
|
-
2. **Lập kế hoạch** — đề xuất subtask, tự phản biện red/blue, rồi **dừng.** Bạn trả lời *"go"* hoặc lái lại.
|
|
136
|
-
3. **Thực thi** — làm từng subtask một, TDD, mỗi subtask một commit (hoặc rải subtask ra cho nhiều agent chạy song song qua Agent OS). Secret / `.env` bị chặn ở gate.
|
|
137
|
-
4. **Kiểm chứng** — chạy test của bạn + một lượt `/dw:review` (tính đúng · bảo mật · convention).
|
|
138
|
-
5. **Đóng** — ghi handoff để session ngày mai tiếp tục mà bạn không phải giải thích lại.
|
|
139
|
-
|
|
140
|
-
Thích kiểm soát thủ công? Tự chạy từng phase và duyệt giữa mỗi bước:
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
/dw:plan → (bạn review) → /dw:execute → /dw:review → /dw:commit
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## Chọn độ sâu (depth)
|
|
149
|
-
|
|
150
|
-
Không có cỡ nào vừa cho tất cả. Đặt mặc định, override theo từng task khi rủi ro tăng.
|
|
151
|
-
|
|
152
|
-
| Depth | Hợp với | Quy trình |
|
|
153
|
-
|-------|---------|-----------|
|
|
154
|
-
| `quick` | Hotfix, code quen, solo | Khảo sát → Thực thi → Đóng |
|
|
155
|
-
| `standard` | Tính năng mới, team nhỏ | Đủ 6 phase |
|
|
156
|
-
| `thorough` | Thay đổi API / DB / bảo mật | Đủ quy trình + arch-review + test-plan + debate |
|
|
157
|
-
|
|
158
|
-
```yaml
|
|
159
|
-
# .dw/config/dw.config.yml
|
|
160
|
-
workflow:
|
|
161
|
-
default_depth: "standard"
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## Lệnh cốt lõi
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
dw init [--solo] # wizard thiết lập / preset (solo · team · enterprise)
|
|
170
|
-
dw doctor # kiểm tra sức khỏe cài đặt
|
|
171
|
-
dw upgrade [--check] # cập nhật file toolkit (giữ nguyên tùy biến của bạn)
|
|
172
|
-
dw task new <name> # tạo khung task doc v3
|
|
173
|
-
dw task show [name] # ảnh chụp ANSI của một task
|
|
174
|
-
dw goal status # tiến độ Goals → Key Results
|
|
175
|
-
dw security-scan # supply-chain guard kiểu AI-native (OSV + heuristic)
|
|
176
|
-
dw dashboard # task đang chạy + ADR + tóm tắt telemetry
|
|
177
|
-
dw metrics # telemetry chỉ-cục-bộ (tắt bằng: DW_NO_TELEMETRY=1)
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Tham chiếu lệnh đầy đủ và danh mục skill nằm trong [`docs/`](docs/).
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Thứ được thêm vào repo của bạn
|
|
185
|
-
|
|
186
|
-
```
|
|
187
|
-
.dw/
|
|
188
|
-
core/ methodology + PILLARS.md
|
|
189
|
-
config/ dw.config.yml (+ tùy chọn .local.yml)
|
|
190
|
-
decisions/ ADR — bản ghi quyết định kiến trúc
|
|
191
|
-
tasks/ task docs + ACTIVE.md index
|
|
192
|
-
metrics/ telemetry cục bộ (có thể opt-out)
|
|
193
|
-
.claude/ skills, hooks, agents, rules cho AI IDE của bạn
|
|
194
|
-
CLAUDE.md ngữ cảnh project agent đọc mỗi session
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Tất cả là Markdown + YAML thuần — kiểm toán được, diff được, và là của bạn.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## 5 trụ cột
|
|
202
|
-
|
|
203
|
-
dw-kit là một **lớp quản trị ưu tiên ngữ cảnh** (context-first), không phải cỗ máy áp đặt. Năm trụ cột theo động từ:
|
|
204
|
-
|
|
205
|
-
| Trụ cột | Mục đích | Ví dụ |
|
|
206
|
-
|---------|----------|-------|
|
|
207
|
-
| **Guards** | An toàn trước khi hành động | `privacy-block`, `pre-commit-gate` |
|
|
208
|
-
| **Surfaces** | Làm trạng thái hiện rõ | `ACTIVE.md`, `dw dashboard` |
|
|
209
|
-
| **Records** | Lưu giữ ký ức | ADR, task docs |
|
|
210
|
-
| **Bridges** | Liền mạch qua các session | auto-handoff, `task.md` |
|
|
211
|
-
| **Tunes** | Thích nghi theo hình hài team | preset, config flag |
|
|
212
|
-
|
|
213
|
-
**Nguyên tắc thiết kế — phép thử lỗi thời:** mỗi tính năng phải trả lời được *"liệu nó có giá trị **hơn** khi AI thông minh hơn không?"* Nếu không, nó bị cắt.
|
|
214
|
-
|
|
215
|
-
Đặc tả đầy đủ: [`.dw/core/PILLARS.md`](.dw/core/PILLARS.md).
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## Yêu cầu
|
|
220
|
-
|
|
221
|
-
- **Node.js ≥ 18**
|
|
222
|
-
- Một công cụ code dạng agentic có CLI — [Claude Code](https://claude.ai/code) (harness đầy đủ) hoặc bất kỳ agent / IDE nào khác qua generic adapter (lớp methodology)
|
|
223
|
-
|
|
224
|
-
## Liên kết
|
|
225
|
-
|
|
226
|
-
[Changelog](CHANGELOG.md) · [Releases](https://github.com/dv-workflow/dv-workflow/releases) · [Chính sách bảo mật](SECURITY.md) · [Giấy phép (Apache-2.0)](LICENSE)
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
Thực hiện tận tâm bởi [huygdv](mailto:huygdv19@gmail.com) · [Báo lỗi](https://github.com/dv-workflow/dv-workflow/issues)
|