@dougharness/cli 1.0.2 → 1.1.1
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/dist/bin.js +18 -3
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +14 -7
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +42 -9
- package/dist/board.js.map +1 -1
- package/dist/detect/repo.js +14 -1
- package/dist/detect/repo.js.map +1 -1
- package/dist/detect/types.d.ts +2 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +491 -0
- package/dist/doctor.js.map +1 -0
- package/dist/generate/agents.d.ts +7 -0
- package/dist/generate/agents.js +62 -12
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +4 -0
- package/dist/generate/claude-md.js.map +1 -1
- package/dist/generate/config.d.ts +8 -0
- package/dist/generate/config.js +4 -1
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +15 -6
- package/dist/generate/proposal.js.map +1 -1
- package/dist/generate/settings.d.ts +2 -0
- package/dist/generate/settings.js +17 -2
- package/dist/generate/settings.js.map +1 -1
- package/dist/init.d.ts +14 -0
- package/dist/init.js +94 -8
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +28 -0
- package/dist/memory-setup.js +326 -0
- package/dist/memory-setup.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
- package/node_modules/@dougharness/flow/agents/lead.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +3 -3
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
- package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +171 -13
- package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
- package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +155 -7
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +188 -24
- package/node_modules/@dougharness/flow/lib/land.mjs +74 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +4 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +368 -50
- package/node_modules/@dougharness/flow/lib/plan.mjs +11 -1
- package/node_modules/@dougharness/flow/package.json +2 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +86 -25
- package/node_modules/@dougharness/flow/scripts/context.mjs +50 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +197 -37
- package/node_modules/@dougharness/flow/scripts/plan.mjs +54 -64
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +105 -0
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +50 -14
- package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +7 -7
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +14 -13
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +145 -86
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +21 -14
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +94 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +40 -7
- package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +558 -197
- package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
- package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
- package/node_modules/@dougharness/gates/scripts/verify.mjs +201 -0
- package/package.json +4 -4
- package/templates/board-app.js +4 -3
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
- package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +0 -92
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
|
@@ -20,6 +20,7 @@ import { statSync } from "node:fs";
|
|
|
20
20
|
import { runHook, allow, addContext } from "../lib/io.mjs";
|
|
21
21
|
import { loadConfig, projectDir } from "../lib/config.mjs";
|
|
22
22
|
import { appendTrace, traceLine } from "../lib/trace.mjs";
|
|
23
|
+
import { writeAgentStart } from "../lib/baseline.mjs";
|
|
23
24
|
import { loadState, saveState } from "../lib/state.mjs";
|
|
24
25
|
import { agentTranscriptPath, contextReading, recordAgentContext, shouldNotify, noticeText } from "../lib/agent-context.mjs";
|
|
25
26
|
|
|
@@ -51,7 +52,10 @@ runHook("trace", async (input) => {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
appendTrace(dir, traceLine(input, { context: reading ? reading.pct : null }));
|
|
55
|
+
appendTrace(dir, traceLine(input, { context: reading ? reading.pct : null, secrets: cfg.secrets }));
|
|
56
|
+
|
|
57
|
+
// card gate-skip-readonly-researchers: record the tree fingerprint the agent starts from (never throws).
|
|
58
|
+
if (input.hook_event_name === "SubagentStart" && input.agent_id) writeAgentStart(dir, cfg, input.session_id, input.agent_id);
|
|
55
59
|
|
|
56
60
|
if (!reading) return allow();
|
|
57
61
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// UserPromptSubmit: record the working tree's fingerprint at the start of a turn (card
|
|
3
|
+
// stop-gate-runs-only-on-code-turns), so a main Stop can tell a read-only turn from one that changed code
|
|
4
|
+
// (lib/baseline.mjs turnTreeStatus; scripts/stop-gate.mjs). Fires on typed prompts and on turns Claude Code
|
|
5
|
+
// starts itself (a background subagent reporting back). Written whatever trace.enabled says. Prints nothing:
|
|
6
|
+
// UserPromptSubmit stdout is added to the model's context. Never blocks; never throws (runHook fails open).
|
|
7
|
+
import { runHook, allow } from "../lib/io.mjs";
|
|
8
|
+
import { loadConfig, projectDir } from "../lib/config.mjs";
|
|
9
|
+
import { writeTurnStart } from "../lib/baseline.mjs";
|
|
10
|
+
|
|
11
|
+
runHook("turn-start", async (input) => {
|
|
12
|
+
// A subagent's own firing (agent_id set) must never write or overwrite the lead's record.
|
|
13
|
+
if (input && input.agent_id) return allow();
|
|
14
|
+
const dir = projectDir(input);
|
|
15
|
+
writeTurnStart(dir, loadConfig(dir), input.session_id);
|
|
16
|
+
return allow();
|
|
17
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run the gate commands (stopGate.commands, or --command), or credit a verified-tree record of the same tree
|
|
3
|
+
// (card verified-tree-record). Usage:
|
|
4
|
+
// node .doug/hooks/scripts/verify.mjs [--no-credit] [--json] [--dir <project>] [--cwd <tree dir>] [--timeout-ms <n>]
|
|
5
|
+
// [--record-dir <dir>] [--credit-dir <dir>]... [--by <writer>] [--command <cmd>]...
|
|
6
|
+
// Exit: 0 all green or credited; 1 any command failed or timed out; 2 usage error or no commands.
|
|
7
|
+
// No count ratchet and no scans here (no session baseline); the Stop gate does those. Takes the Stop gate's .doug/.state/gate.lock around the commands (card verify-takes-gate-lock).
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { unlinkSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { loadConfig } from "../lib/config.mjs";
|
|
13
|
+
import { gateLockPath, readGateLock, pidIsLive, writeGateLock, takeOverGateLock, resolveHookTimeoutSec, lockStaleByAge, normalizedTimeoutMs, lockWaitBoundMs } from "../lib/gate-lock.mjs";
|
|
14
|
+
import { runCommand, tail } from "../lib/run.mjs";
|
|
15
|
+
import { parseTotals, readSummaries, sumTotals } from "../lib/test-totals.mjs";
|
|
16
|
+
import { computeTree, readRecord, writeRecord, removeRecord, creditable, resolveCommands } from "../lib/verified-tree.mjs";
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const o = { noCredit: false, json: false, commands: [], creditDirs: [] };
|
|
20
|
+
const val = (i) => (i + 1 < argv.length ? argv[i + 1] : null);
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === "--no-credit") o.noCredit = true;
|
|
24
|
+
else if (a === "--json") o.json = true;
|
|
25
|
+
else if (a === "--dir" || a === "--cwd" || a === "--record-dir" || a === "--credit-dir" || a === "--command" || a === "--by" || a === "--timeout-ms") {
|
|
26
|
+
const v = val(i);
|
|
27
|
+
if (v === null) return { error: `${a} needs a value` };
|
|
28
|
+
i++;
|
|
29
|
+
if (a === "--command") o.commands.push(v);
|
|
30
|
+
else if (a === "--credit-dir") o.creditDirs.push(v);
|
|
31
|
+
else if (a === "--timeout-ms") {
|
|
32
|
+
const n = Number(v);
|
|
33
|
+
if (!Number.isInteger(n) || n <= 0) return { error: "--timeout-ms needs a positive integer" };
|
|
34
|
+
o.timeoutMs = n;
|
|
35
|
+
}
|
|
36
|
+
else o[a === "--dir" ? "dir" : a === "--cwd" ? "cwd" : a === "--by" ? "by" : "recordDir"] = v;
|
|
37
|
+
} else return { error: `unknown argument ${a}` };
|
|
38
|
+
}
|
|
39
|
+
return o;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toplevel() {
|
|
43
|
+
try {
|
|
44
|
+
const env = {};
|
|
45
|
+
for (const [k, v] of Object.entries(process.env)) if (!k.startsWith("GIT_")) env[k] = v;
|
|
46
|
+
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd(), encoding: "utf8", env, timeout: 10000 });
|
|
47
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
48
|
+
} catch {
|
|
49
|
+
// fall through
|
|
50
|
+
}
|
|
51
|
+
return process.cwd();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function passed(totals) {
|
|
55
|
+
const s = totals && totals.status === "ok" ? readSummaries(totals) : null;
|
|
56
|
+
if (!s) return null;
|
|
57
|
+
const t = sumTotals(s);
|
|
58
|
+
return t.total - t.skipped - t.todo;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function gateLine(entries) {
|
|
62
|
+
return "gate: " + entries.map((e) => `${e.name} ${e.exit}${e.passed === null || e.passed === undefined ? "" : ` (${e.passed} passed)`}`).join("; ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function main() {
|
|
66
|
+
const o = parseArgs(process.argv.slice(2));
|
|
67
|
+
if (o.error) {
|
|
68
|
+
process.stderr.write(`[doug] verify: ${o.error}\n`);
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
const dir = resolve(o.dir || toplevel());
|
|
72
|
+
const cwd = resolve(o.cwd || dir);
|
|
73
|
+
const recordDir = resolve(o.recordDir || dir);
|
|
74
|
+
const cfg = loadConfig(dir);
|
|
75
|
+
const commands = o.commands.length ? o.commands.map((c) => ({ name: c, command: c })) : resolveCommands(cfg);
|
|
76
|
+
if (commands.length === 0) {
|
|
77
|
+
process.stderr.write("[doug] verify: no commands (stopGate.commands is empty and no --command given)\n");
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
let treeBefore = computeTree(cwd);
|
|
81
|
+
|
|
82
|
+
// Credits a verified-tree record of the same tree when one exists; returns the exit code, or null.
|
|
83
|
+
const tryCredit = () => {
|
|
84
|
+
if (!o.noCredit && treeBefore) {
|
|
85
|
+
const wanted = commands.map((c) => c.command);
|
|
86
|
+
let record = null;
|
|
87
|
+
let fromOther = false;
|
|
88
|
+
for (const [i, d] of [recordDir, ...o.creditDirs.map((c) => resolve(c))].entries()) {
|
|
89
|
+
const rec = readRecord(d);
|
|
90
|
+
if (rec && creditable(cwd, cfg, rec, treeBefore, wanted).ok) {
|
|
91
|
+
record = rec;
|
|
92
|
+
fromOther = i > 0;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (record) {
|
|
97
|
+
if (fromOther) writeRecord(recordDir, { ...record, tree: treeBefore, by: `${o.by || "verify"} (from ${record.by || "unknown"})`, at: new Date().toISOString() });
|
|
98
|
+
const entries = record.commands.map((c) => ({ name: c.name, exit: c.exit, passed: passed(record.totals[c.command]) }));
|
|
99
|
+
if (o.json) {
|
|
100
|
+
const results = record.commands.map((c) => ({ name: c.name, command: c.command, ok: true, status: c.exit, timedOut: false, durationMs: c.durationMs, outputTail: "" }));
|
|
101
|
+
process.stdout.write(JSON.stringify({ ok: true, credited: "verified-tree", tree: treeBefore, record, results }) + "\n");
|
|
102
|
+
} else {
|
|
103
|
+
process.stdout.write(`[doug] verify: credited verified-tree record (tree ${treeBefore.slice(0, 7)}, by ${record.by || "unknown"}, ${record.at}, node ${record.node}); commands not re-run. --no-credit runs them.\n`);
|
|
104
|
+
process.stdout.write(gateLine(entries) + "\n");
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
const first = tryCredit();
|
|
112
|
+
if (first !== null) return first;
|
|
113
|
+
|
|
114
|
+
const timeoutMs = o.timeoutMs ? o.timeoutMs : (cfg.stopGate && normalizedTimeoutMs(cfg)) ?? 600000;
|
|
115
|
+
const waitBoundMs = lockWaitBoundMs(timeoutMs, commands.length);
|
|
116
|
+
|
|
117
|
+
// Card verify-takes-gate-lock: one full suite at a time per checkout, the same gate.lock the Stop gate takes
|
|
118
|
+
// (lib/gate-lock.mjs). A live holder is waited on for up to timeoutMs x command count (the Stop gate's bound), then the record is re-checked (the
|
|
119
|
+
// holder's green run may have just written it); a lock still held after the bound is taken over and the
|
|
120
|
+
// commands run. Released only by the exit handler below, so every return path (green, red, throw) frees it.
|
|
121
|
+
let lockOwned = false;
|
|
122
|
+
process.on("exit", () => {
|
|
123
|
+
try {
|
|
124
|
+
if (!lockOwned) return;
|
|
125
|
+
const lock = readGateLock(dir);
|
|
126
|
+
if (lock && lock.pid === process.pid) unlinkSync(gateLockPath(dir));
|
|
127
|
+
} catch {
|
|
128
|
+
// never throw from an exit handler
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
writeGateLock(dir, o.by || "verify");
|
|
133
|
+
lockOwned = true;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (!err || err.code !== "EEXIST") {
|
|
136
|
+
lockOwned = false; // an unwritable .doug/.state: run unlocked rather than hang
|
|
137
|
+
} else {
|
|
138
|
+
const hookTimeoutMs = resolveHookTimeoutSec(cfg) * 1000;
|
|
139
|
+
const stale = (l) => !l || !pidIsLive(l.pid) || lockStaleByAge(l, hookTimeoutMs);
|
|
140
|
+
const held = readGateLock(dir);
|
|
141
|
+
if (stale(held)) {
|
|
142
|
+
takeOverGateLock(dir, o.by || "verify");
|
|
143
|
+
lockOwned = true;
|
|
144
|
+
} else {
|
|
145
|
+
const deadline = Date.now() + waitBoundMs;
|
|
146
|
+
let live = held;
|
|
147
|
+
const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
148
|
+
while (true) {
|
|
149
|
+
live = readGateLock(dir);
|
|
150
|
+
if (stale(live) || Date.now() >= deadline) break;
|
|
151
|
+
Atomics.wait(nap, 0, 0, 200);
|
|
152
|
+
}
|
|
153
|
+
treeBefore = computeTree(cwd);
|
|
154
|
+
const again = tryCredit();
|
|
155
|
+
if (again !== null) return again;
|
|
156
|
+
if (!stale(live)) process.stderr.write(`[doug] verify: gate.lock still held by pid ${live.pid} after ${waitBoundMs} ms; running the commands anyway.\n`);
|
|
157
|
+
takeOverGateLock(dir, o.by || "verify");
|
|
158
|
+
lockOwned = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const results = [];
|
|
163
|
+
const recCommands = [];
|
|
164
|
+
const totals = {};
|
|
165
|
+
const entries = [];
|
|
166
|
+
for (const c of commands) {
|
|
167
|
+
const r = runCommand(c.command, { cwd, timeoutMs });
|
|
168
|
+
const exit = r.status === null || r.status === undefined ? 1 : r.status;
|
|
169
|
+
const parsed = r.ok ? parseTotals(r.stdout + "\n" + r.stderr) : { status: "none" };
|
|
170
|
+
results.push({ name: c.name, command: c.command, ok: r.ok, status: r.status, timedOut: r.timedOut, durationMs: r.durationMs, outputTail: tail(`${r.stdout}\n${r.stderr}`) });
|
|
171
|
+
recCommands.push({ name: c.name, command: c.command, exit, durationMs: r.durationMs });
|
|
172
|
+
if (r.ok && parsed.status === "ok") totals[c.command] = parsed;
|
|
173
|
+
entries.push({ name: c.name, exit, passed: r.ok ? passed(parsed) : null });
|
|
174
|
+
if (!o.json) {
|
|
175
|
+
process.stdout.write(`[doug] verify: \`${c.command}\` exit ${r.timedOut ? "timeout" : exit} (${(r.durationMs / 1000).toFixed(1)} s)\n`);
|
|
176
|
+
if (!r.ok) process.stdout.write(`${r.stderr}${r.stdout}\n`);
|
|
177
|
+
}
|
|
178
|
+
if (!r.ok) break;
|
|
179
|
+
}
|
|
180
|
+
const ok = results.length === commands.length && results.every((r) => r.ok);
|
|
181
|
+
if (ok) {
|
|
182
|
+
const treeAfter = computeTree(cwd);
|
|
183
|
+
if (treeBefore && treeAfter && treeBefore === treeAfter) {
|
|
184
|
+
writeRecord(recordDir, { version: 1, tree: treeAfter, node: process.version, at: new Date().toISOString(), by: o.by || "verify", commands: recCommands, totals });
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
removeRecord(recordDir);
|
|
188
|
+
}
|
|
189
|
+
if (o.json) process.stdout.write(JSON.stringify({ ok, credited: null, tree: treeBefore, record: null, results }) + "\n");
|
|
190
|
+
else process.stdout.write(gateLine(entries) + "\n");
|
|
191
|
+
return ok ? 0 : 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let code = 1;
|
|
195
|
+
try {
|
|
196
|
+
code = main();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
process.stderr.write(`[doug] verify: ${err && err.message ? err.message : err}\n`);
|
|
199
|
+
code = 1;
|
|
200
|
+
}
|
|
201
|
+
process.exitCode = code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougharness/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "doug: tailored, approval-gated harness install for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"doug": "
|
|
16
|
+
"doug": "dist/bin.js"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"node": ">=22.16"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@dougharness/flow": "1.
|
|
27
|
-
"@dougharness/gates": "1.
|
|
26
|
+
"@dougharness/flow": "1.1.1",
|
|
27
|
+
"@dougharness/gates": "1.1.1"
|
|
28
28
|
},
|
|
29
29
|
"bundleDependencies": [
|
|
30
30
|
"@dougharness/flow",
|
package/templates/board-app.js
CHANGED
|
@@ -56,8 +56,8 @@ function cardHtml(c, columns) {
|
|
|
56
56
|
const deps = (c.deps || []).length ? `<div class="meta">after <code>${c.deps.map(esc).join("</code> <code>")}</code></div>` : "";
|
|
57
57
|
const opts = columns.map((col) => `<option value="${col.id}"${col.id === c.column ? " selected" : ""}>${esc(col.title)}</option>`).join("");
|
|
58
58
|
const chip = c.component ? `<span class="chip" style="color:var(--c-${esc(c.component)})">${esc(c.component)}</span>` : "";
|
|
59
|
-
// A hand-track card is
|
|
60
|
-
const hand = c.track === "hand" ? `<span class="chip hand" title="hand track:
|
|
59
|
+
// A hand-track card is a gated by-hand change; the chip says so at a glance.
|
|
60
|
+
const hand = c.track === "hand" ? `<span class="chip hand" title="hand track: a gated by-hand change">by hand</span>` : "";
|
|
61
61
|
const tags = c.tags && c.tags.length ? c.tags.map((t) => `<span class="chip tag">${esc(t)}</span>`).join("") : "";
|
|
62
62
|
const cls = c.class ? `<span class="chip class" title="class">${esc(c.class)}</span>` : "";
|
|
63
63
|
const compAttr = c.component ? ` data-component="${esc(c.component)}"` : "";
|
|
@@ -339,7 +339,8 @@ function app() {
|
|
|
339
339
|
});
|
|
340
340
|
if (res.status === 200) {
|
|
341
341
|
board = await res.json();
|
|
342
|
-
|
|
342
|
+
const warning = res.headers.get("x-doug-warning");
|
|
343
|
+
say("Saved to " + config.record + "." + (warning ? " " + warning : ""));
|
|
343
344
|
} else {
|
|
344
345
|
let message = "The move was rejected (" + res.status + ").";
|
|
345
346
|
try { const body = await res.json(); if (body && body.error) message = body.error; } catch (_) {}
|