cclaw-cli 0.51.27 → 0.51.29
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/artifact-linter.js +73 -16
- package/dist/cli.d.ts +17 -1
- package/dist/cli.js +185 -49
- package/dist/codex-feature-flag.d.ts +1 -1
- package/dist/codex-feature-flag.js +1 -1
- package/dist/config.js +3 -0
- package/dist/content/cancel-command.d.ts +2 -0
- package/dist/content/cancel-command.js +25 -0
- package/dist/content/finish-command.d.ts +2 -0
- package/dist/content/finish-command.js +26 -0
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/hooks.js +32 -9
- package/dist/content/ideate-command.js +12 -7
- package/dist/content/next-command.js +17 -13
- package/dist/content/node-hooks.js +22 -6
- package/dist/content/opencode-plugin.js +1 -1
- package/dist/content/stages/review.js +1 -1
- package/dist/content/stages/tdd.js +1 -1
- package/dist/content/start-command.js +6 -5
- package/dist/content/state-contracts.js +1 -1
- package/dist/content/status-command.js +4 -3
- package/dist/content/track-render-context.d.ts +1 -0
- package/dist/content/track-render-context.js +2 -0
- package/dist/doctor-registry.d.ts +2 -0
- package/dist/doctor-registry.js +37 -10
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.js +183 -2
- package/dist/fs-utils.js +6 -0
- package/dist/harness-adapters.js +29 -5
- package/dist/install.d.ts +4 -1
- package/dist/install.js +37 -4
- package/dist/internal/advance-stage.js +6 -6
- package/dist/managed-resources.d.ts +53 -0
- package/dist/managed-resources.js +289 -0
- package/dist/run-archive.d.ts +8 -0
- package/dist/run-archive.js +19 -5
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -1
- package/dist/tdd-cycle.js +10 -10
- package/dist/tdd-verification-evidence.js +4 -4
- package/dist/track-heuristics.d.ts +2 -0
- package/dist/track-heuristics.js +11 -3
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -105,6 +105,24 @@ function sectionBodyByHeadingPrefix(sections, prefix) {
|
|
|
105
105
|
}
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Build a regex that matches `<field>: <value>` even when the field name
|
|
110
|
+
* and/or value are wrapped in markdown emphasis (`*`, `**`, `_`, `__`).
|
|
111
|
+
*
|
|
112
|
+
* The shipped templates render fields as `- **Field name:** value`, so any
|
|
113
|
+
* structural check that searches for `Field:\s*token` against the rendered
|
|
114
|
+
* artifact must tolerate the closing `**` between the colon and the value.
|
|
115
|
+
*
|
|
116
|
+
* `field` is treated as literal text (regex meta-characters are escaped).
|
|
117
|
+
* `value` is inserted verbatim so callers can pass alternation
|
|
118
|
+
* (`STARTUP|BUILDER|...`). `flags` defaults to case-insensitive Unicode.
|
|
119
|
+
*/
|
|
120
|
+
function markdownFieldRegex(field, value, flags = "iu") {
|
|
121
|
+
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
122
|
+
const emph = "[*_]{0,2}";
|
|
123
|
+
const source = `(?:^|[\\s>])${emph}\\s*${escapedField}\\s*${emph}\\s*:\\s*${emph}\\s*(?:${value})\\b`;
|
|
124
|
+
return new RegExp(source, flags);
|
|
125
|
+
}
|
|
108
126
|
export function extractMarkdownSectionBody(markdown, section) {
|
|
109
127
|
return sectionBodyByName(extractH2Sections(markdown), section);
|
|
110
128
|
}
|
|
@@ -211,7 +229,7 @@ function normalizeDesignDiagramTier(value) {
|
|
|
211
229
|
if (!value)
|
|
212
230
|
return null;
|
|
213
231
|
const normalized = value.trim().toLowerCase();
|
|
214
|
-
if (/^
|
|
232
|
+
if (/^(?:lite|light|lightweight)$/u.test(normalized))
|
|
215
233
|
return "lightweight";
|
|
216
234
|
if (/^standard$/u.test(normalized))
|
|
217
235
|
return "standard";
|
|
@@ -224,12 +242,21 @@ function parseApproachTierSection(sectionBody) {
|
|
|
224
242
|
return null;
|
|
225
243
|
for (const line of sectionBody.split(/\r?\n/u)) {
|
|
226
244
|
const cleaned = line.replace(/[*_`]/gu, "").trim();
|
|
227
|
-
const directMatch = /(?:^|\b)tier\s*:\s*(lightweight|light|standard|deep)\b/iu.exec(cleaned);
|
|
245
|
+
const directMatch = /(?:^|\b)tier\s*:\s*(lite|lightweight|light|standard|deep)\b/iu.exec(cleaned);
|
|
228
246
|
if (directMatch) {
|
|
229
|
-
|
|
247
|
+
const captured = directMatch[1] ?? "";
|
|
248
|
+
const remainder = cleaned.slice(cleaned.toLowerCase().indexOf("tier") + 4);
|
|
249
|
+
const tierTokens = remainder.match(/\b(?:lite|lightweight|light|standard|deep)\b/giu) ?? [];
|
|
250
|
+
const distinct = new Set(tierTokens.map((token) => token.toLowerCase()));
|
|
251
|
+
if (distinct.size >= 2) {
|
|
252
|
+
// Multi-token line is the unfilled template placeholder
|
|
253
|
+
// (`Tier: lite | standard | deep`); treat as no decision.
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
return normalizeDesignDiagramTier(captured);
|
|
230
257
|
}
|
|
231
258
|
}
|
|
232
|
-
const token = /\b(lightweight|light|standard|deep)\b/iu.exec(sectionBody)?.[1] ?? null;
|
|
259
|
+
const token = /\b(lite|lightweight|light|standard|deep)\b/iu.exec(sectionBody)?.[1] ?? null;
|
|
233
260
|
return normalizeDesignDiagramTier(token);
|
|
234
261
|
}
|
|
235
262
|
async function resolveDesignDiagramTier(projectRoot, track, designRaw) {
|
|
@@ -1782,15 +1809,31 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
1782
1809
|
// to a single row (defeating the "2-3 distinct approaches" gate).
|
|
1783
1810
|
const tierBody = sectionBodyByName(sections, "Approach Tier");
|
|
1784
1811
|
if (tierBody !== null) {
|
|
1785
|
-
|
|
1812
|
+
// Token vocabulary covers `lite`, `Lightweight`, `Standard`, and
|
|
1813
|
+
// `Deep` (case-insensitive). A line that lists ≥2 distinct tokens is
|
|
1814
|
+
// the unfilled template placeholder (`Tier: lite | standard | deep`)
|
|
1815
|
+
// and must not silently pass; we look for at least one decision line
|
|
1816
|
+
// with exactly one token, while ignoring placeholder lines.
|
|
1817
|
+
const cleanedLines = tierBody
|
|
1818
|
+
.split("\n")
|
|
1819
|
+
.map((line) => line.replace(/[*_`]/gu, ""));
|
|
1820
|
+
const lineTokenCounts = cleanedLines.map((line) => {
|
|
1821
|
+
const tokens = line.match(/\b(?:lite|lightweight|light|standard|deep)\b/giu) ?? [];
|
|
1822
|
+
return new Set(tokens.map((token) => token.toLowerCase())).size;
|
|
1823
|
+
});
|
|
1824
|
+
const hasDecisionLine = lineTokenCounts.some((count) => count === 1);
|
|
1825
|
+
const hasPlaceholderLine = lineTokenCounts.some((count) => count >= 2);
|
|
1826
|
+
const ok = hasDecisionLine;
|
|
1786
1827
|
findings.push({
|
|
1787
1828
|
section: "Approach Tier Classification",
|
|
1788
1829
|
required: true,
|
|
1789
|
-
rule: "Approach Tier must explicitly classify depth as Lightweight, Standard
|
|
1790
|
-
found:
|
|
1791
|
-
details:
|
|
1792
|
-
? "Approach Tier includes a recognized depth token."
|
|
1793
|
-
:
|
|
1830
|
+
rule: "Approach Tier must explicitly classify depth as one of `lite` (a.k.a. `Lightweight`), `Standard`, or `Deep`.",
|
|
1831
|
+
found: ok,
|
|
1832
|
+
details: ok
|
|
1833
|
+
? "Approach Tier includes a single recognized depth token."
|
|
1834
|
+
: hasPlaceholderLine
|
|
1835
|
+
? "Approach Tier still lists multiple tier tokens (template placeholder); pick exactly one of `lite`/`Lightweight`, `Standard`, or `Deep`."
|
|
1836
|
+
: "Approach Tier is missing a recognized depth token (`lite`/`Lightweight`, `Standard`, or `Deep`)."
|
|
1794
1837
|
});
|
|
1795
1838
|
}
|
|
1796
1839
|
const approachesBody = sectionBodyByName(sections, "Approaches");
|
|
@@ -1931,16 +1974,30 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
1931
1974
|
// approach detail cards, anti-sycophancy stamp).
|
|
1932
1975
|
const modeBody = sectionBodyByName(sections, "Mode Block");
|
|
1933
1976
|
if (modeBody !== null) {
|
|
1934
|
-
const
|
|
1935
|
-
const
|
|
1977
|
+
const modeTokens = ["STARTUP", "BUILDER", "ENGINEERING", "OPS", "RESEARCH"];
|
|
1978
|
+
const modeRegex = markdownFieldRegex("Mode", modeTokens.join("|"), "u");
|
|
1979
|
+
const tokenMatches = new Set();
|
|
1980
|
+
const lineRegex = new RegExp(modeRegex.source, "gu");
|
|
1981
|
+
for (const match of modeBody.matchAll(lineRegex)) {
|
|
1982
|
+
const token = (match[0].match(/STARTUP|BUILDER|ENGINEERING|OPS|RESEARCH/u) ?? [""])[0];
|
|
1983
|
+
if (token)
|
|
1984
|
+
tokenMatches.add(token);
|
|
1985
|
+
}
|
|
1986
|
+
const placeholderLine = modeBody
|
|
1987
|
+
.split("\n")
|
|
1988
|
+
.find((line) => /\bMode\b\s*[*_]{0,2}\s*:/iu.test(line) && (line.match(/STARTUP|BUILDER|ENGINEERING|OPS|RESEARCH/giu) ?? []).length >= 2);
|
|
1989
|
+
const isPlaceholder = Boolean(placeholderLine);
|
|
1990
|
+
const ok = tokenMatches.size === 1 && !isPlaceholder;
|
|
1936
1991
|
findings.push({
|
|
1937
1992
|
section: "Mode Block Token",
|
|
1938
1993
|
required: true,
|
|
1939
1994
|
rule: "Mode Block must declare exactly one mode token: STARTUP, BUILDER, ENGINEERING, OPS, or RESEARCH.",
|
|
1940
1995
|
found: ok,
|
|
1941
1996
|
details: ok
|
|
1942
|
-
?
|
|
1943
|
-
:
|
|
1997
|
+
? `Recognized mode token detected: ${[...tokenMatches][0] ?? ""}.`
|
|
1998
|
+
: isPlaceholder
|
|
1999
|
+
? "Mode Block still lists multiple mode tokens (template placeholder); pick exactly one of STARTUP/BUILDER/ENGINEERING/OPS/RESEARCH."
|
|
2000
|
+
: "Mode Block is missing a recognized mode token (STARTUP/BUILDER/ENGINEERING/OPS/RESEARCH)."
|
|
1944
2001
|
});
|
|
1945
2002
|
}
|
|
1946
2003
|
const forcingBody = sectionBodyByName(sections, "Forcing Questions");
|
|
@@ -2038,7 +2095,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
2038
2095
|
}
|
|
2039
2096
|
const stampBody = sectionBodyByName(sections, "Anti-Sycophancy Stamp");
|
|
2040
2097
|
if (stampBody !== null) {
|
|
2041
|
-
const acknowledged =
|
|
2098
|
+
const acknowledged = markdownFieldRegex("Forbidden response openers acknowledged", "yes|true|y").test(stampBody);
|
|
2042
2099
|
findings.push({
|
|
2043
2100
|
section: "Anti-Sycophancy Acknowledgement",
|
|
2044
2101
|
required: true,
|
|
@@ -2137,7 +2194,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
2137
2194
|
}
|
|
2138
2195
|
const regressionBody = sectionBodyByName(sections, "Regression Iron Rule");
|
|
2139
2196
|
if (regressionBody !== null) {
|
|
2140
|
-
const ack =
|
|
2197
|
+
const ack = markdownFieldRegex("Iron rule acknowledged", "yes|true|y").test(regressionBody);
|
|
2141
2198
|
findings.push({
|
|
2142
2199
|
section: "Regression Iron Rule Acknowledgement",
|
|
2143
2200
|
required: true,
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { FlowTrack, HarnessId } from "./types.js";
|
|
3
|
+
import type { ArchiveDisposition } from "./runs.js";
|
|
3
4
|
type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive" | "internal";
|
|
4
5
|
interface ParsedArgs {
|
|
5
6
|
command?: CommandName;
|
|
@@ -15,6 +16,8 @@ interface ParsedArgs {
|
|
|
15
16
|
archiveName?: string;
|
|
16
17
|
archiveSkipRetro?: boolean;
|
|
17
18
|
archiveSkipRetroReason?: string;
|
|
19
|
+
archiveDisposition?: ArchiveDisposition;
|
|
20
|
+
archiveDispositionReason?: string;
|
|
18
21
|
/** Hidden plumbing command (`cclaw internal ...`) arguments. */
|
|
19
22
|
internalArgs?: string[];
|
|
20
23
|
showHelp?: boolean;
|
|
@@ -23,5 +26,18 @@ interface ParsedArgs {
|
|
|
23
26
|
export declare function usage(): string;
|
|
24
27
|
declare function parseHarnesses(raw: string): HarnessId[];
|
|
25
28
|
declare function parseTrack(raw: string): FlowTrack;
|
|
29
|
+
declare function parseArchiveDisposition(raw: string): ArchiveDisposition;
|
|
30
|
+
export type HarnessSelectionAnswer = {
|
|
31
|
+
kind: "accept";
|
|
32
|
+
} | {
|
|
33
|
+
kind: "all";
|
|
34
|
+
} | {
|
|
35
|
+
kind: "toggle";
|
|
36
|
+
indexes: number[];
|
|
37
|
+
} | {
|
|
38
|
+
kind: "invalid";
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function parseHarnessSelectionAnswer(raw: string): HarnessSelectionAnswer;
|
|
26
42
|
declare function parseArgs(argv: string[]): ParsedArgs;
|
|
27
|
-
export { parseArgs, parseHarnesses, parseTrack };
|
|
43
|
+
export { parseArgs, parseArchiveDisposition, parseHarnesses, parseTrack };
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@ import { doctorChecks, doctorSucceeded } from "./doctor.js";
|
|
|
8
8
|
import { initCclaw, syncCclaw, uninstallCclaw, upgradeCclaw } from "./install.js";
|
|
9
9
|
import { error, info } from "./logger.js";
|
|
10
10
|
import { FLOW_TRACKS, HARNESS_IDS } from "./types.js";
|
|
11
|
-
import { archiveRun } from "./runs.js";
|
|
11
|
+
import { ARCHIVE_DISPOSITIONS, archiveRun } from "./runs.js";
|
|
12
12
|
import { CCLAW_VERSION, RUNTIME_ROOT } from "./constants.js";
|
|
13
|
-
import { createDefaultConfig } from "./config.js";
|
|
13
|
+
import { createDefaultConfig, readConfig } from "./config.js";
|
|
14
14
|
import { detectHarnesses } from "./init-detect.js";
|
|
15
15
|
import { HARNESS_ADAPTERS } from "./harness-adapters.js";
|
|
16
16
|
import { classifyCodexHooksFlag, codexConfigPath, patchCodexHooksFlag, readCodexConfig, writeCodexConfig } from "./codex-feature-flag.js";
|
|
@@ -38,17 +38,21 @@ Commands:
|
|
|
38
38
|
Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
|
|
39
39
|
--no-interactive Skip interactive prompts even on TTY (for CI/scripts).
|
|
40
40
|
sync Reconcile generated runtime files with the current config.
|
|
41
|
+
Flags: --harnesses=<list> Update configured harnesses before syncing.
|
|
42
|
+
--interactive Pick harnesses from a numbered TTY menu.
|
|
41
43
|
doctor Check install/runtime wiring and print concrete fixes for failures.
|
|
42
44
|
Flags: --explain Include docs pointers for every check.
|
|
43
45
|
--json Emit machine-readable check results.
|
|
44
46
|
--quiet Show only failing checks.
|
|
45
47
|
--only=<filter> Limit displayed checks (error,warning,hook:,state:,...).
|
|
46
|
-
--reconcile-gates Refresh derived gate status before checking.
|
|
48
|
+
--reconcile-gates Refresh derived gate status before checking; does not repair missing artifacts/tests.
|
|
47
49
|
upgrade Refresh generated files in .cclaw. Preserves your config.yaml.
|
|
48
50
|
archive Archive the active run and reset flow state for the next run.
|
|
49
51
|
Flags: --name=<slug> Override archive folder suffix.
|
|
50
52
|
--skip-retro Skip retro gate only when runtime allows it.
|
|
51
53
|
--retro-reason=<txt> Required rationale with --skip-retro.
|
|
54
|
+
--disposition=<completed|cancelled|abandoned>
|
|
55
|
+
--reason=<txt> Required for cancelled/abandoned archives.
|
|
52
56
|
uninstall Remove .cclaw runtime and the generated harness shim files.
|
|
53
57
|
|
|
54
58
|
Global flags:
|
|
@@ -58,17 +62,18 @@ Global flags:
|
|
|
58
62
|
Examples:
|
|
59
63
|
npx cclaw-cli
|
|
60
64
|
npx cclaw-cli init --harnesses=claude,cursor --no-interactive
|
|
61
|
-
npx cclaw-cli sync
|
|
65
|
+
npx cclaw-cli sync --interactive
|
|
62
66
|
npx cclaw-cli archive --name=my-run
|
|
67
|
+
npx cclaw-cli archive --disposition=cancelled --reason="deprioritized"
|
|
63
68
|
npx cclaw-cli upgrade
|
|
64
69
|
|
|
65
70
|
Happy-path work happens inside your harness via /cc, /cc-next,
|
|
66
|
-
/cc-ideate, and /cc-
|
|
71
|
+
/cc-ideate, /cc-view, /cc-finish, and /cc-cancel. Doctor is an operator/support surface:
|
|
67
72
|
it verifies install/runtime wiring, but a real harness smoke test is
|
|
68
73
|
still needed to prove provider auth and model execution.
|
|
69
74
|
|
|
70
75
|
Docs: https://github.com/zuevrs/cclaw
|
|
71
|
-
Local:
|
|
76
|
+
Local: README.md and generated .cclaw/skills/*.md
|
|
72
77
|
Issues: https://github.com/zuevrs/cclaw/issues
|
|
73
78
|
`;
|
|
74
79
|
}
|
|
@@ -77,6 +82,9 @@ function parseHarnesses(raw) {
|
|
|
77
82
|
.split(",")
|
|
78
83
|
.map((item) => item.trim())
|
|
79
84
|
.filter(Boolean);
|
|
85
|
+
if (requested.length === 0) {
|
|
86
|
+
throw new Error("Select at least one harness.");
|
|
87
|
+
}
|
|
80
88
|
const invalid = requested.filter((item) => !HARNESS_IDS.includes(item));
|
|
81
89
|
if (invalid.length > 0) {
|
|
82
90
|
throw new Error(`Unknown harnesses: ${invalid.join(", ")}`);
|
|
@@ -90,6 +98,13 @@ function parseTrack(raw) {
|
|
|
90
98
|
}
|
|
91
99
|
return trimmed;
|
|
92
100
|
}
|
|
101
|
+
function parseArchiveDisposition(raw) {
|
|
102
|
+
const trimmed = raw.trim();
|
|
103
|
+
if (!ARCHIVE_DISPOSITIONS.includes(trimmed)) {
|
|
104
|
+
throw new Error(`Unknown archive disposition: ${trimmed}. Supported: ${ARCHIVE_DISPOSITIONS.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
return trimmed;
|
|
107
|
+
}
|
|
93
108
|
function isInitPromptAllowed(ctx) {
|
|
94
109
|
return Boolean(process.stdin.isTTY && ctx.stdout.isTTY);
|
|
95
110
|
}
|
|
@@ -156,39 +171,95 @@ function buildInitSurfacePreview(harnesses) {
|
|
|
156
171
|
}
|
|
157
172
|
return lines;
|
|
158
173
|
}
|
|
159
|
-
|
|
174
|
+
function harnessLabel(harness) {
|
|
175
|
+
const adapter = HARNESS_ADAPTERS[harness];
|
|
176
|
+
const tier = adapter ? `${adapter.reality.declaredSupport}, ${adapter.capabilities.hookSurface} hooks` : "supported";
|
|
177
|
+
return `${harness} (${tier})`;
|
|
178
|
+
}
|
|
179
|
+
function selectedHarnessPreview(harnesses) {
|
|
180
|
+
return harnesses.length > 0 ? harnesses.join(", ") : "none";
|
|
181
|
+
}
|
|
182
|
+
export function parseHarnessSelectionAnswer(raw) {
|
|
183
|
+
const answer = raw.trim().toLowerCase();
|
|
184
|
+
if (answer.length === 0)
|
|
185
|
+
return { kind: "accept" };
|
|
186
|
+
if (answer === "all")
|
|
187
|
+
return { kind: "all" };
|
|
188
|
+
if (answer === "none") {
|
|
189
|
+
return { kind: "invalid", message: "Zero harnesses is not supported. Select at least one harness." };
|
|
190
|
+
}
|
|
191
|
+
const parts = answer.split(",").map((part) => part.trim()).filter(Boolean);
|
|
192
|
+
const indexes = parts.map((part) => Number.parseInt(part, 10));
|
|
193
|
+
if (indexes.some((value) => !Number.isInteger(value) || value < 1 || value > HARNESS_IDS.length)) {
|
|
194
|
+
return { kind: "invalid", message: `Invalid selection. Use numbers 1-${HARNESS_IDS.length}, comma-separated.` };
|
|
195
|
+
}
|
|
196
|
+
return { kind: "toggle", indexes };
|
|
197
|
+
}
|
|
198
|
+
async function promptHarnessSelection(defaults, ctx, label = "Harness selection") {
|
|
160
199
|
const rl = createInterface({
|
|
161
200
|
input: process.stdin,
|
|
162
201
|
output: ctx.stdout
|
|
163
202
|
});
|
|
164
|
-
const
|
|
165
|
-
|
|
203
|
+
const defaultSet = new Set(defaults.harnesses);
|
|
204
|
+
const selected = new Set(defaults.harnesses.length > 0 ? defaults.harnesses : HARNESS_IDS);
|
|
205
|
+
const detected = new Set(defaults.detectedHarnesses ?? []);
|
|
206
|
+
const current = new Set(defaults.currentHarnesses ?? []);
|
|
207
|
+
const printMenu = () => {
|
|
208
|
+
ctx.stdout.write(`\n${label}\n`);
|
|
209
|
+
ctx.stdout.write(`Detected: ${selectedHarnessPreview(defaults.detectedHarnesses ?? [])}\n`);
|
|
210
|
+
ctx.stdout.write(`Current: ${selectedHarnessPreview(defaults.currentHarnesses ?? [])}\n`);
|
|
211
|
+
ctx.stdout.write(`Supported harnesses and target paths:\n`);
|
|
212
|
+
HARNESS_IDS.forEach((harness, index) => {
|
|
213
|
+
const adapter = HARNESS_ADAPTERS[harness];
|
|
214
|
+
const markers = [
|
|
215
|
+
detected.has(harness) ? "detected" : "",
|
|
216
|
+
current.has(harness) ? "current" : "",
|
|
217
|
+
defaultSet.has(harness) ? "default" : ""
|
|
218
|
+
].filter(Boolean).join(", ");
|
|
219
|
+
const checked = selected.has(harness) ? "x" : " ";
|
|
220
|
+
ctx.stdout.write(` ${index + 1}. [${checked}] ${harnessLabel(harness)} -> ${adapter.commandDir}${markers ? ` (${markers})` : ""}\n`);
|
|
221
|
+
});
|
|
222
|
+
ctx.stdout.write("Enter numbers to toggle (for example 1,3), 'all', or press Enter to accept.\n");
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
166
225
|
while (true) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const parsed = parseHarnesses(answer);
|
|
173
|
-
if (parsed.length === 0) {
|
|
226
|
+
printMenu();
|
|
227
|
+
const answer = await rl.question(`Selected [${[...selected].join(",") || "select at least one"}]: `);
|
|
228
|
+
const parsedAnswer = parseHarnessSelectionAnswer(answer);
|
|
229
|
+
if (parsedAnswer.kind === "accept") {
|
|
230
|
+
if (selected.size === 0) {
|
|
174
231
|
ctx.stdout.write("Select at least one harness.\n");
|
|
175
232
|
continue;
|
|
176
233
|
}
|
|
177
|
-
return
|
|
234
|
+
return HARNESS_IDS.filter((harness) => selected.has(harness));
|
|
235
|
+
}
|
|
236
|
+
if (parsedAnswer.kind === "all") {
|
|
237
|
+
HARNESS_IDS.forEach((harness) => selected.add(harness));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (parsedAnswer.kind === "invalid") {
|
|
241
|
+
ctx.stdout.write(`${parsedAnswer.message}\n`);
|
|
242
|
+
continue;
|
|
178
243
|
}
|
|
179
|
-
|
|
180
|
-
|
|
244
|
+
for (const index of parsedAnswer.indexes) {
|
|
245
|
+
const harness = HARNESS_IDS[index - 1];
|
|
246
|
+
if (!harness)
|
|
247
|
+
continue;
|
|
248
|
+
if (selected.has(harness))
|
|
249
|
+
selected.delete(harness);
|
|
250
|
+
else
|
|
251
|
+
selected.add(harness);
|
|
181
252
|
}
|
|
182
253
|
}
|
|
183
|
-
};
|
|
184
|
-
try {
|
|
185
|
-
const harnesses = await pickHarnesses(defaults.harnesses);
|
|
186
|
-
return { harnesses };
|
|
187
254
|
}
|
|
188
255
|
finally {
|
|
189
256
|
rl.close();
|
|
190
257
|
}
|
|
191
258
|
}
|
|
259
|
+
async function promptInitConfig(defaults, ctx) {
|
|
260
|
+
const harnesses = await promptHarnessSelection(defaults, ctx, "Initial cclaw harnesses");
|
|
261
|
+
return { harnesses };
|
|
262
|
+
}
|
|
192
263
|
/**
|
|
193
264
|
* When Codex is one of the installed harnesses, check the Codex CLI
|
|
194
265
|
* config file for the `codex_hooks` feature flag. If it is missing or
|
|
@@ -293,13 +364,41 @@ async function resolveInitInputs(parsed, ctx) {
|
|
|
293
364
|
const defaults = {
|
|
294
365
|
harnesses: autoHarnesses ?? HARNESS_IDS.slice()
|
|
295
366
|
};
|
|
296
|
-
const prompted = await promptInitConfig(defaults, ctx);
|
|
367
|
+
const prompted = await promptInitConfig({ ...defaults, detectedHarnesses }, ctx);
|
|
297
368
|
return {
|
|
298
369
|
track: parsed.track,
|
|
299
370
|
harnesses: prompted.harnesses,
|
|
300
371
|
detectedHarnesses
|
|
301
372
|
};
|
|
302
373
|
}
|
|
374
|
+
async function resolveSyncInputs(parsed, ctx) {
|
|
375
|
+
const explicitHarnesses = parsed.harnesses;
|
|
376
|
+
if (explicitHarnesses && explicitHarnesses.length > 0) {
|
|
377
|
+
return { harnesses: explicitHarnesses };
|
|
378
|
+
}
|
|
379
|
+
if (parsed.interactive !== true) {
|
|
380
|
+
return {};
|
|
381
|
+
}
|
|
382
|
+
if (!isInitPromptAllowed(ctx)) {
|
|
383
|
+
throw new Error("Interactive sync requires a TTY. Remove --interactive or run in a terminal.");
|
|
384
|
+
}
|
|
385
|
+
let currentHarnesses = [];
|
|
386
|
+
try {
|
|
387
|
+
currentHarnesses = (await readConfig(ctx.cwd)).harnesses;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
currentHarnesses = [];
|
|
391
|
+
}
|
|
392
|
+
const detectedHarnesses = await detectHarnesses(ctx.cwd);
|
|
393
|
+
const defaults = detectedHarnesses.length > 0 ? detectedHarnesses : currentHarnesses.length > 0 ? currentHarnesses : HARNESS_IDS.slice();
|
|
394
|
+
return {
|
|
395
|
+
harnesses: await promptHarnessSelection({
|
|
396
|
+
harnesses: defaults,
|
|
397
|
+
detectedHarnesses,
|
|
398
|
+
currentHarnesses
|
|
399
|
+
}, ctx, "Sync harness reconfiguration")
|
|
400
|
+
};
|
|
401
|
+
}
|
|
303
402
|
function parseDoctorOnly(raw) {
|
|
304
403
|
return raw
|
|
305
404
|
.split(",")
|
|
@@ -335,24 +434,47 @@ function doctorCountsBySeverity(checks) {
|
|
|
335
434
|
}
|
|
336
435
|
return result;
|
|
337
436
|
}
|
|
437
|
+
const DOCTOR_ACTION_GROUP_LABELS = {
|
|
438
|
+
sync: "Can fix with cclaw sync",
|
|
439
|
+
"user-action": "Requires user action",
|
|
440
|
+
"stage-work": "Requires stage work",
|
|
441
|
+
informational: "Informational warning"
|
|
442
|
+
};
|
|
443
|
+
function doctorActionGroupOrder(group) {
|
|
444
|
+
return group === "sync" ? 0 : group === "user-action" ? 1 : group === "stage-work" ? 2 : 3;
|
|
445
|
+
}
|
|
338
446
|
function printDoctorText(ctx, checks, options) {
|
|
339
447
|
const orderedSeverities = ["error", "warning", "info"];
|
|
340
448
|
const view = options.quiet ? checks.filter((check) => !check.ok) : checks;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
449
|
+
const actionGroups = [...new Set(view.map((check) => check.actionGroup))]
|
|
450
|
+
.sort((left, right) => doctorActionGroupOrder(left) - doctorActionGroupOrder(right));
|
|
451
|
+
for (const actionGroup of actionGroups) {
|
|
452
|
+
const groupChecks = view.filter((check) => check.actionGroup === actionGroup);
|
|
453
|
+
const failingInGroup = groupChecks.filter((check) => !check.ok).length;
|
|
454
|
+
ctx.stdout.write(`
|
|
455
|
+
[${DOCTOR_ACTION_GROUP_LABELS[actionGroup]}] ${failingInGroup}/${groupChecks.length} failing
|
|
456
|
+
`);
|
|
457
|
+
for (const severity of orderedSeverities) {
|
|
458
|
+
const inBucket = groupChecks.filter((check) => check.severity === severity);
|
|
459
|
+
if (inBucket.length === 0)
|
|
460
|
+
continue;
|
|
461
|
+
ctx.stdout.write(` ${severity.toUpperCase()}
|
|
462
|
+
`);
|
|
463
|
+
for (const check of inBucket) {
|
|
464
|
+
const status = check.ok ? "PASS" : "FAIL";
|
|
465
|
+
ctx.stdout.write(` ${status} ${check.name} :: ${check.summary}
|
|
466
|
+
`);
|
|
467
|
+
if (!options.quiet) {
|
|
468
|
+
ctx.stdout.write(` details: ${check.details}
|
|
469
|
+
`);
|
|
470
|
+
}
|
|
471
|
+
if (!check.ok || options.explain) {
|
|
472
|
+
ctx.stdout.write(` next action: ${check.fix}
|
|
473
|
+
`);
|
|
474
|
+
if (check.docRef) {
|
|
475
|
+
ctx.stdout.write(` reference: ${check.docRef}
|
|
476
|
+
`);
|
|
477
|
+
}
|
|
356
478
|
}
|
|
357
479
|
}
|
|
358
480
|
}
|
|
@@ -392,13 +514,13 @@ function parseArgs(argv) {
|
|
|
392
514
|
}
|
|
393
515
|
const flags = rest;
|
|
394
516
|
const isAllowedForCommand = (flag) => {
|
|
395
|
-
if (parsed.command === "init") {
|
|
517
|
+
if (parsed.command === "init" || parsed.command === "sync") {
|
|
396
518
|
return flag.startsWith("--harnesses=") ||
|
|
397
|
-
flag.startsWith("--track=") ||
|
|
398
|
-
flag.startsWith("--profile=") ||
|
|
519
|
+
(parsed.command === "init" && flag.startsWith("--track=")) ||
|
|
520
|
+
(parsed.command === "init" && flag.startsWith("--profile=")) ||
|
|
399
521
|
flag === "--interactive" ||
|
|
400
522
|
flag === "--no-interactive" ||
|
|
401
|
-
flag === "--dry-run";
|
|
523
|
+
(parsed.command === "init" && flag === "--dry-run");
|
|
402
524
|
}
|
|
403
525
|
if (parsed.command === "doctor") {
|
|
404
526
|
return flag === "--reconcile-gates" ||
|
|
@@ -410,7 +532,9 @@ function parseArgs(argv) {
|
|
|
410
532
|
if (parsed.command === "archive") {
|
|
411
533
|
return flag.startsWith("--name=") ||
|
|
412
534
|
flag === "--skip-retro" ||
|
|
413
|
-
flag.startsWith("--retro-reason=")
|
|
535
|
+
flag.startsWith("--retro-reason=") ||
|
|
536
|
+
flag.startsWith("--disposition=") ||
|
|
537
|
+
flag.startsWith("--reason=");
|
|
414
538
|
}
|
|
415
539
|
return false;
|
|
416
540
|
};
|
|
@@ -473,6 +597,14 @@ function parseArgs(argv) {
|
|
|
473
597
|
parsed.archiveSkipRetroReason = flag.replace("--retro-reason=", "").trim();
|
|
474
598
|
continue;
|
|
475
599
|
}
|
|
600
|
+
if (flag.startsWith("--disposition=")) {
|
|
601
|
+
parsed.archiveDisposition = parseArchiveDisposition(flag.replace("--disposition=", ""));
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (flag.startsWith("--reason=")) {
|
|
605
|
+
parsed.archiveDispositionReason = flag.replace("--reason=", "").trim();
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
476
608
|
}
|
|
477
609
|
return parsed;
|
|
478
610
|
}
|
|
@@ -532,8 +664,10 @@ async function runCommand(parsed, ctx) {
|
|
|
532
664
|
return 0;
|
|
533
665
|
}
|
|
534
666
|
if (command === "sync") {
|
|
535
|
-
await
|
|
536
|
-
|
|
667
|
+
const resolved = await resolveSyncInputs(parsed, ctx);
|
|
668
|
+
await syncCclaw(ctx.cwd, { harnesses: resolved.harnesses });
|
|
669
|
+
const harnessNote = resolved.harnesses ? ` (${resolved.harnesses.join(", ")})` : "";
|
|
670
|
+
info(ctx, `Synchronized harness shims from current .cclaw config${harnessNote}`);
|
|
537
671
|
return 0;
|
|
538
672
|
}
|
|
539
673
|
if (command === "doctor") {
|
|
@@ -571,12 +705,14 @@ async function runCommand(parsed, ctx) {
|
|
|
571
705
|
if (command === "archive") {
|
|
572
706
|
const archived = await archiveRun(ctx.cwd, parsed.archiveName, {
|
|
573
707
|
skipRetro: parsed.archiveSkipRetro === true,
|
|
574
|
-
skipRetroReason: parsed.archiveSkipRetroReason
|
|
708
|
+
skipRetroReason: parsed.archiveSkipRetroReason,
|
|
709
|
+
disposition: parsed.archiveDisposition,
|
|
710
|
+
dispositionReason: parsed.archiveDispositionReason
|
|
575
711
|
});
|
|
576
712
|
const snapshotSummary = archived.snapshottedStateFiles.length > 0
|
|
577
713
|
? ` Snapshotted ${archived.snapshottedStateFiles.length} state file(s) under ${archived.archivePath}/state and wrote archive-manifest.json.`
|
|
578
714
|
: "";
|
|
579
|
-
info(ctx, `Archived active artifacts to ${archived.archivePath}. Flow state reset to brainstorm.${snapshotSummary}`);
|
|
715
|
+
info(ctx, `Archived active artifacts to ${archived.archivePath} (${archived.disposition}). Flow state reset to brainstorm.${snapshotSummary}`);
|
|
580
716
|
const k = archived.knowledge;
|
|
581
717
|
if (k.overThreshold) {
|
|
582
718
|
info(ctx, `Knowledge curation recommended: ${k.knowledgePath} now has ${k.activeEntryCount} active entries (soft threshold ${k.softThreshold}). Ask your harness to curate cclaw knowledge and plan a soft-archive of stale/duplicate entries to ${RUNTIME_ROOT}/knowledge.archive.jsonl.`);
|
|
@@ -624,4 +760,4 @@ function isDirectExecution() {
|
|
|
624
760
|
if (isDirectExecution()) {
|
|
625
761
|
void main();
|
|
626
762
|
}
|
|
627
|
-
export { parseArgs, parseHarnesses, parseTrack };
|
|
763
|
+
export { parseArgs, parseArchiveDisposition, parseHarnesses, parseTrack };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
13
|
* in `$CODEX_HOME/config.toml` (default: `~/.codex/config.toml`).
|
|
14
|
-
* cclaw
|
|
14
|
+
* cclaw init/sync can prompt the user to flip this flag for them; `cclaw doctor --explain` reports the concrete repair when it is missing;
|
|
15
15
|
* this module owns the detection / mutation code so the prompt logic in
|
|
16
16
|
* `cli.ts` stays small and testable.
|
|
17
17
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
13
|
* in `$CODEX_HOME/config.toml` (default: `~/.codex/config.toml`).
|
|
14
|
-
* cclaw
|
|
14
|
+
* cclaw init/sync can prompt the user to flip this flag for them; `cclaw doctor --explain` reports the concrete repair when it is missing;
|
|
15
15
|
* this module owns the detection / mutation code so the prompt logic in
|
|
16
16
|
* `cli.ts` stays small and testable.
|
|
17
17
|
*
|
package/dist/config.js
CHANGED
|
@@ -245,6 +245,9 @@ export async function readConfig(projectRoot, options = {}) {
|
|
|
245
245
|
throw configValidationError(fullPath, `unknown harness id(s): ${formatted}`);
|
|
246
246
|
}
|
|
247
247
|
const validatedHarnesses = configuredHarnesses;
|
|
248
|
+
if (hasHarnessesField && validatedHarnesses.length === 0) {
|
|
249
|
+
throw configValidationError(fullPath, `"harnesses" must include at least one harness`);
|
|
250
|
+
}
|
|
248
251
|
const harnesses = hasHarnessesField
|
|
249
252
|
? [...new Set(validatedHarnesses)]
|
|
250
253
|
: DEFAULT_HARNESSES;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function cancelCommandContract() {
|
|
2
|
+
return `# /cc-cancel command contract
|
|
3
|
+
|
|
4
|
+
Use this command when the user wants to stop the active run without claiming completion.
|
|
5
|
+
|
|
6
|
+
## Protocol
|
|
7
|
+
|
|
8
|
+
1. Ask for a concise cancellation reason if the user has not already provided one.
|
|
9
|
+
2. Run \`cclaw archive --disposition=cancelled --reason=<reason>\` from the project root. Use \`--disposition=abandoned\` only when the user explicitly frames the run as abandoned rather than cancelled.
|
|
10
|
+
3. Report the archive path and reset run id. Make clear that the archived run is not a completed ship.
|
|
11
|
+
|
|
12
|
+
Cancelled and abandoned archives are allowed from any stage, but they require a required reason so future readers know why the run ended.
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
export function cancelCommandSkillMarkdown() {
|
|
16
|
+
return `---
|
|
17
|
+
name: flow-cancel
|
|
18
|
+
description: Cancel or abandon the active cclaw run with a required reason. Use when the user types /cc-cancel or asks to cancel, abandon, stop, discard, or reset an unfinished run.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Cancel cclaw Run
|
|
22
|
+
|
|
23
|
+
Load and follow \`.cclaw/commands/cancel.md\`. This is a non-completion path: require a reason and archive with cancelled or abandoned disposition.
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function finishCommandContract() {
|
|
2
|
+
return `# /cc-finish command contract
|
|
3
|
+
|
|
4
|
+
Use this command when the user says the active run is complete and wants to close it out.
|
|
5
|
+
|
|
6
|
+
## Protocol
|
|
7
|
+
|
|
8
|
+
1. Read \`.cclaw/state/flow-state.json\` and \`.cclaw/commands/next.md\`.
|
|
9
|
+
2. Confirm ship closeout is \`ready_to_archive\`. If not, route to \`/cc-next\` until retro and compound closeout are complete or explicitly skipped there.
|
|
10
|
+
3. Run \`cclaw archive --disposition=completed\` from the project root.
|
|
11
|
+
4. Report the archive path, reset run id, and any knowledge curation hint printed by the CLI.
|
|
12
|
+
|
|
13
|
+
Completed archives keep strict closeout gates: do not bypass retro or compound review from this command.
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
export function finishCommandSkillMarkdown() {
|
|
17
|
+
return `---
|
|
18
|
+
name: flow-finish
|
|
19
|
+
description: Finish a completed cclaw run by archiving with completed disposition. Use when the user types /cc-finish or asks to finish, close, complete, or archive a successful run.
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Finish cclaw Run
|
|
23
|
+
|
|
24
|
+
Load and follow \`.cclaw/commands/finish.md\`. This is the successful closeout path and must preserve the normal ship closeout gates before archive.
|
|
25
|
+
`;
|
|
26
|
+
}
|