@popoverai/dotrequirements 0.24.1 → 0.24.3
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/README.md +1 -1
- package/dist/codebase-to-spec/cache.d.ts +6 -0
- package/dist/codebase-to-spec/cache.js +1 -0
- package/dist/codebase-to-spec/claude.d.ts +1 -0
- package/dist/codebase-to-spec/claude.js +9 -0
- package/dist/codebase-to-spec/dispatch.d.ts +69 -0
- package/dist/codebase-to-spec/dispatch.js +484 -0
- package/dist/codebase-to-spec/pack.d.ts +16 -0
- package/dist/codebase-to-spec/pack.js +17 -3
- package/dist/codebase-to-spec/present.d.ts +8 -1
- package/dist/codebase-to-spec/present.js +7 -4
- package/dist/codebase-to-spec/progress.d.ts +6 -0
- package/dist/codebase-to-spec/progress.js +34 -0
- package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/outline-reviewer.js +3 -1
- package/dist/codebase-to-spec/prompts/planner-initial.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/planner-initial.js +4 -0
- package/dist/codebase-to-spec/prompts/planner-revise.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/planner-revise.js +2 -2
- package/dist/codebase-to-spec/prompts/spec-reviewer.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/spec-reviewer.js +6 -1
- package/dist/codebase-to-spec/prompts/specifier.d.ts +1 -1
- package/dist/codebase-to-spec/prompts/specifier.js +6 -4
- package/dist/codebase-to-spec/prompts/style-check.d.ts +10 -2
- package/dist/codebase-to-spec/prompts/style-check.js +76 -46
- package/dist/codebase-to-spec/schemas.d.ts +460 -1
- package/dist/codebase-to-spec/schemas.js +158 -1
- package/dist/codebase-to-spec/skill-install.d.ts +36 -12
- package/dist/codebase-to-spec/skill-install.js +127 -26
- package/dist/codebase-to-spec/specifier.js +6 -0
- package/dist/commands/codebase-to-spec/compose-orchestrator.d.ts +14 -0
- package/dist/commands/codebase-to-spec/compose-orchestrator.js +54 -0
- package/dist/commands/codebase-to-spec/dispatch-context.d.ts +12 -0
- package/dist/commands/codebase-to-spec/dispatch-context.js +22 -0
- package/dist/commands/codebase-to-spec/dispatch-editor.d.ts +16 -0
- package/dist/commands/codebase-to-spec/dispatch-editor.js +71 -0
- package/dist/commands/codebase-to-spec/dispatch-planner.d.ts +19 -0
- package/dist/commands/codebase-to-spec/dispatch-planner.js +90 -0
- package/dist/commands/codebase-to-spec/dispatch-spec.d.ts +16 -0
- package/dist/commands/codebase-to-spec/dispatch-spec.js +59 -0
- package/dist/commands/codebase-to-spec/index.js +69 -1
- package/dist/commands/codebase-to-spec/pack.d.ts +6 -0
- package/dist/commands/codebase-to-spec/pack.js +1 -0
- package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +20 -0
- package/dist/commands/codebase-to-spec/present-orchestrator.js +81 -0
- package/dist/commands/codebase-to-spec/present.d.ts +5 -0
- package/dist/commands/codebase-to-spec/present.js +6 -1
- package/dist/commands/codebase-to-spec/run.js +1 -0
- package/dist/commands/codebase-to-spec/skill-install.js +12 -1
- package/dist/templates/agents/cts-worker.md +9 -0
- package/dist/templates/hooks/cts-worker-persona.sh +76 -0
- package/dist/templates/skills/codebase-to-spec/SKILL.md +159 -68
- package/package.json +4 -5
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
// ---------- Outline ----------
|
|
13
|
+
export const CustomerSchema = z.object({
|
|
14
|
+
name: z.string().min(1),
|
|
15
|
+
description: z.string().min(1),
|
|
16
|
+
});
|
|
13
17
|
export const AreaSchema = z.object({
|
|
14
18
|
name: z.string().min(1),
|
|
15
19
|
description: z.string().min(1),
|
|
@@ -17,6 +21,13 @@ export const AreaSchema = z.object({
|
|
|
17
21
|
.string()
|
|
18
22
|
.regex(/^[A-Z][A-Z0-9_]*$/, "prefix must be uppercase alphanumeric/underscore"),
|
|
19
23
|
files: z.array(z.string()).min(0),
|
|
24
|
+
/**
|
|
25
|
+
* Customers this area serves — at least one. Each customer is a *user* of
|
|
26
|
+
* the software (not a contributor to its codebase). The specifier will use
|
|
27
|
+
* one of these as the persona it grounds the area's requirements in.
|
|
28
|
+
* See CTS-PLAN-1 (customer threading).
|
|
29
|
+
*/
|
|
30
|
+
customers: z.array(CustomerSchema).min(1),
|
|
20
31
|
});
|
|
21
32
|
export const OutlineSchema = z.object({
|
|
22
33
|
title: z.string().min(1),
|
|
@@ -45,8 +56,21 @@ export const OUTLINE_JSON_SCHEMA = {
|
|
|
45
56
|
description: { type: "string" },
|
|
46
57
|
prefix: { type: "string" },
|
|
47
58
|
files: { type: "array", items: { type: "string" } },
|
|
59
|
+
customers: {
|
|
60
|
+
type: "array",
|
|
61
|
+
minItems: 1,
|
|
62
|
+
items: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
name: { type: "string" },
|
|
66
|
+
description: { type: "string" },
|
|
67
|
+
},
|
|
68
|
+
required: ["name", "description"],
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
48
72
|
},
|
|
49
|
-
required: ["name", "description", "prefix", "files"],
|
|
73
|
+
required: ["name", "description", "prefix", "files", "customers"],
|
|
50
74
|
additionalProperties: false,
|
|
51
75
|
},
|
|
52
76
|
},
|
|
@@ -165,6 +189,139 @@ export const SPEC_REVIEW_JSON_SCHEMA = {
|
|
|
165
189
|
],
|
|
166
190
|
additionalProperties: false,
|
|
167
191
|
};
|
|
192
|
+
// ---------- Conversational orchestrator outline (Phase 2b refactor) ----------
|
|
193
|
+
//
|
|
194
|
+
// The conversational orchestrator uses a single evolving `outline.yaml` as
|
|
195
|
+
// the pipeline's substrate. The outline carries both the behavioral spec
|
|
196
|
+
// content (areas, customers, source_files) AND its own lifecycle state
|
|
197
|
+
// (review.result + review.thread). This is distinct from the legacy
|
|
198
|
+
// `OutlineSchema` above, which the `cts run` pipeline still uses.
|
|
199
|
+
//
|
|
200
|
+
// Field naming uses "result" rather than "verdict" deliberately: review is
|
|
201
|
+
// a collaborative interaction with the worker, not a juridical ruling.
|
|
202
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
203
|
+
export const ConversationalCustomerSchema = z.object({
|
|
204
|
+
description: z.string().min(1),
|
|
205
|
+
});
|
|
206
|
+
/**
|
|
207
|
+
* One entry in the review thread (project-level or per-area). Discriminated
|
|
208
|
+
* on `result`:
|
|
209
|
+
*
|
|
210
|
+
* - `approved`: no revisions required.
|
|
211
|
+
* - `needs-revision`: must include a non-empty `revisions` list — each
|
|
212
|
+
* entry is a clear, actionable instruction for the next planner pass.
|
|
213
|
+
*/
|
|
214
|
+
export const ConversationalReviewEntrySchema = z.discriminatedUnion("result", [
|
|
215
|
+
z.object({
|
|
216
|
+
result: z.literal("approved"),
|
|
217
|
+
}),
|
|
218
|
+
z.object({
|
|
219
|
+
result: z.literal("needs-revision"),
|
|
220
|
+
revisions: z.array(z.string()).min(1),
|
|
221
|
+
}),
|
|
222
|
+
]);
|
|
223
|
+
/**
|
|
224
|
+
* Review state. Used both at the project level (outline.review) and per
|
|
225
|
+
* area (area.review). `result` mirrors the latest thread entry's result
|
|
226
|
+
* so callers can query state without walking the thread.
|
|
227
|
+
*/
|
|
228
|
+
export const ConversationalReviewSchema = z.object({
|
|
229
|
+
result: z.enum(["approved", "needs-revision"]),
|
|
230
|
+
thread: z.array(ConversationalReviewEntrySchema).min(1),
|
|
231
|
+
});
|
|
232
|
+
export const ConversationalAreaSchema = z.object({
|
|
233
|
+
name: z.string().min(1),
|
|
234
|
+
prefix: z
|
|
235
|
+
.string()
|
|
236
|
+
.regex(/^[A-Z][A-Z0-9_]*$/, "prefix must be uppercase alphanumeric/underscore"),
|
|
237
|
+
description: z.string().min(1),
|
|
238
|
+
source_files: z.array(z.string()).min(0),
|
|
239
|
+
customers: z.array(ConversationalCustomerSchema).min(1),
|
|
240
|
+
/**
|
|
241
|
+
* Per-area review state — same shape as the project-level review.
|
|
242
|
+
* Optional: planner output doesn't include it; CA adds it after reviewing
|
|
243
|
+
* a partial. Tracks the per-area iteration loop (specify → review →
|
|
244
|
+
* editor → re-review → approved) the same way `outline.review` tracks
|
|
245
|
+
* the outline iteration loop.
|
|
246
|
+
*/
|
|
247
|
+
review: ConversationalReviewSchema.optional(),
|
|
248
|
+
});
|
|
249
|
+
export const ConversationalOutlineSchema = z.object({
|
|
250
|
+
title: z.string().min(1),
|
|
251
|
+
defaultPrefix: z
|
|
252
|
+
.string()
|
|
253
|
+
.regex(/^[A-Z][A-Z0-9_]*$/, "defaultPrefix must be uppercase alphanumeric/underscore"),
|
|
254
|
+
summary: z.string().min(1),
|
|
255
|
+
review: ConversationalReviewSchema.optional(),
|
|
256
|
+
areas: z.array(ConversationalAreaSchema).min(1),
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* Parse YAML text against the conversational outline schema. Throws with
|
|
260
|
+
* a descriptive message on parse or validation failure.
|
|
261
|
+
*/
|
|
262
|
+
export function parseConversationalOutline(text) {
|
|
263
|
+
let raw;
|
|
264
|
+
try {
|
|
265
|
+
raw = parseYaml(text);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
throw new Error(`Outline YAML is invalid: ${err instanceof Error ? err.message : String(err)}`);
|
|
269
|
+
}
|
|
270
|
+
return ConversationalOutlineSchema.parse(raw);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Synthesize a short customer `name` from a conversational customer's
|
|
274
|
+
* `description`. The orchestrator's `ConversationalCustomerSchema` only
|
|
275
|
+
* carries description, but the legacy `CustomerSchema` requires both
|
|
276
|
+
* `name` and `description`. Adapter use only — downstream compose/present
|
|
277
|
+
* stages don't read `name`, so this is a schema-shape bridge, not a
|
|
278
|
+
* semantic field.
|
|
279
|
+
*
|
|
280
|
+
* Heuristic: descriptions like "Priya, a CTS pipeline operator" yield
|
|
281
|
+
* "Priya" via first comma-segment. Otherwise truncate to 40 chars.
|
|
282
|
+
*/
|
|
283
|
+
function synthesizeCustomerName(description) {
|
|
284
|
+
const firstSegment = description.split(",")[0]?.trim() ?? "";
|
|
285
|
+
if (firstSegment.length > 0 && firstSegment.length <= 40) {
|
|
286
|
+
return firstSegment;
|
|
287
|
+
}
|
|
288
|
+
return description.slice(0, 40).trim() || description;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Adapt a conversational orchestrator outline to the legacy Outline shape
|
|
292
|
+
* for use with stages that still take the legacy schema (compose, present).
|
|
293
|
+
*
|
|
294
|
+
* Discards orchestrator-only fields (review); maps `source_files` → `files`;
|
|
295
|
+
* bridges the customer-shape difference by synthesizing a `name` from each
|
|
296
|
+
* conversational customer's description.
|
|
297
|
+
*/
|
|
298
|
+
export function conversationalOutlineToLegacy(outline) {
|
|
299
|
+
return {
|
|
300
|
+
title: outline.title,
|
|
301
|
+
defaultPrefix: outline.defaultPrefix,
|
|
302
|
+
summary: outline.summary,
|
|
303
|
+
areas: outline.areas.map((a) => ({
|
|
304
|
+
name: a.name,
|
|
305
|
+
description: a.description,
|
|
306
|
+
prefix: a.prefix,
|
|
307
|
+
files: a.source_files,
|
|
308
|
+
customers: a.customers.map((c) => ({
|
|
309
|
+
name: synthesizeCustomerName(c.description),
|
|
310
|
+
description: c.description,
|
|
311
|
+
})),
|
|
312
|
+
})),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Serialize a conversational outline back to YAML text. Uses literal-block
|
|
317
|
+
* multi-line strings (`|`) where possible for readability.
|
|
318
|
+
*/
|
|
319
|
+
export function stringifyConversationalOutline(outline) {
|
|
320
|
+
return stringifyYaml(outline, {
|
|
321
|
+
lineWidth: 0,
|
|
322
|
+
blockQuote: "literal",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
168
325
|
export function parseSpecReview(text) {
|
|
169
326
|
const trimmed = text.trim();
|
|
170
327
|
let raw;
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill installation logic for the codebase-to-spec skill.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Ships as a bundle: SKILL.md + cts-worker agent definition + persona-
|
|
5
|
+
* injection hook script + hook registration in `.claude/settings.local.json`.
|
|
6
|
+
* The skill body (the conversational orchestrator) DEPENDS on the companion
|
|
7
|
+
* files — installing just the skill without them would leave Task dispatches
|
|
8
|
+
* unable to resolve their persona. For `project` scope, all four pieces are
|
|
9
|
+
* installed in one shot. For `global` and `custom` scopes, only the skill
|
|
10
|
+
* body is installed; the companions are project-scoped by CC convention.
|
|
8
11
|
*
|
|
9
12
|
* Host portability (CTS-SKILL-5): the install logic supports any host that
|
|
10
13
|
* follows the Agent Skills format. The default target is Claude Code's
|
|
11
14
|
* convention; `--target-dir` lets users place the skill anywhere.
|
|
12
15
|
*
|
|
13
16
|
* Requirements covered:
|
|
14
|
-
* - CTS-SKILL-1
|
|
15
|
-
* -
|
|
16
|
-
* Agent Skills format support
|
|
17
|
+
* - CTS-SKILL-1, CTS-SKILL-5
|
|
18
|
+
* - CTSO-CLI-1 (bundles the agent + hook that PreToolUse-injects persona bodies)
|
|
17
19
|
*/
|
|
18
20
|
export type SkillInstallScope = "project" | "global" | "custom";
|
|
19
21
|
export interface SkillInstallOptions {
|
|
20
22
|
/**
|
|
21
23
|
* Where to install:
|
|
22
24
|
* - `project` (default): `<projectRoot>/.claude/skills/codebase-to-spec/`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* (plus companions under `<projectRoot>/.claude/agents/` and
|
|
26
|
+
* `<projectRoot>/.claude/hooks/`, and hook registration in
|
|
27
|
+
* `<projectRoot>/.claude/settings.local.json`)
|
|
28
|
+
* - `global`: `~/.claude/skills/codebase-to-spec/` (skill body only)
|
|
29
|
+
* - `custom`: requires `targetDir` (skill body only)
|
|
25
30
|
*/
|
|
26
31
|
scope?: SkillInstallScope;
|
|
27
32
|
/** Required when `scope === 'custom'`. The skill directory will be created here. */
|
|
28
33
|
targetDir?: string;
|
|
29
34
|
/** Project root (used when scope === 'project'). Defaults to cwd. */
|
|
30
35
|
projectRoot?: string;
|
|
31
|
-
/** Overwrite existing SKILL.md if present. Default false. */
|
|
36
|
+
/** Overwrite existing SKILL.md / agent / hook if present. Default false. */
|
|
32
37
|
overwrite?: boolean;
|
|
33
38
|
}
|
|
34
39
|
export interface SkillInstallResult {
|
|
@@ -38,6 +43,13 @@ export interface SkillInstallResult {
|
|
|
38
43
|
installed: boolean;
|
|
39
44
|
/** True if an existing file was replaced. */
|
|
40
45
|
overwrote: boolean;
|
|
46
|
+
/** Companion files installed alongside the skill (project scope only). */
|
|
47
|
+
companions?: {
|
|
48
|
+
agentPath: string;
|
|
49
|
+
hookScriptPath: string;
|
|
50
|
+
settingsPath: string;
|
|
51
|
+
hookRegistered: boolean;
|
|
52
|
+
};
|
|
41
53
|
}
|
|
42
54
|
/**
|
|
43
55
|
* Resolve the directory in which the skill should live, given the user's
|
|
@@ -50,8 +62,20 @@ export declare function resolveSkillDir(options: SkillInstallOptions): string;
|
|
|
50
62
|
*/
|
|
51
63
|
export declare function loadSkillTemplate(): string;
|
|
52
64
|
/**
|
|
53
|
-
*
|
|
54
|
-
|
|
65
|
+
* Load the bundled cts-worker agent definition template.
|
|
66
|
+
*/
|
|
67
|
+
export declare function loadAgentTemplate(): string;
|
|
68
|
+
/**
|
|
69
|
+
* Load the bundled persona-injection hook script template.
|
|
70
|
+
*/
|
|
71
|
+
export declare function loadHookTemplate(): string;
|
|
72
|
+
/**
|
|
73
|
+
* Install (or refuse to overwrite) the codebase-to-spec skill bundle.
|
|
74
|
+
*
|
|
75
|
+
* For `project` scope: installs skill body, cts-worker agent, persona hook
|
|
76
|
+
* script, and registers the hook in settings.local.json.
|
|
77
|
+
*
|
|
78
|
+
* For `global` and `custom` scopes: installs only the skill body.
|
|
55
79
|
*/
|
|
56
80
|
export declare function installSkill(options?: SkillInstallOptions): SkillInstallResult;
|
|
57
81
|
//# sourceMappingURL=skill-install.d.ts.map
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill installation logic for the codebase-to-spec skill.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Ships as a bundle: SKILL.md + cts-worker agent definition + persona-
|
|
5
|
+
* injection hook script + hook registration in `.claude/settings.local.json`.
|
|
6
|
+
* The skill body (the conversational orchestrator) DEPENDS on the companion
|
|
7
|
+
* files — installing just the skill without them would leave Task dispatches
|
|
8
|
+
* unable to resolve their persona. For `project` scope, all four pieces are
|
|
9
|
+
* installed in one shot. For `global` and `custom` scopes, only the skill
|
|
10
|
+
* body is installed; the companions are project-scoped by CC convention.
|
|
8
11
|
*
|
|
9
12
|
* Host portability (CTS-SKILL-5): the install logic supports any host that
|
|
10
13
|
* follows the Agent Skills format. The default target is Claude Code's
|
|
11
14
|
* convention; `--target-dir` lets users place the skill anywhere.
|
|
12
15
|
*
|
|
13
16
|
* Requirements covered:
|
|
14
|
-
* - CTS-SKILL-1
|
|
15
|
-
* -
|
|
16
|
-
* Agent Skills format support
|
|
17
|
+
* - CTS-SKILL-1, CTS-SKILL-5
|
|
18
|
+
* - CTSO-CLI-1 (bundles the agent + hook that PreToolUse-injects persona bodies)
|
|
17
19
|
*/
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
19
21
|
import { homedir } from "node:os";
|
|
20
22
|
import { dirname, join, resolve } from "node:path";
|
|
21
23
|
import { loadTemplate } from "../utils/templates.js";
|
|
@@ -49,31 +51,130 @@ export function loadSkillTemplate() {
|
|
|
49
51
|
return loadTemplate("skills/codebase-to-spec/SKILL.md");
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
+
* Load the bundled cts-worker agent definition template.
|
|
55
|
+
*/
|
|
56
|
+
export function loadAgentTemplate() {
|
|
57
|
+
return loadTemplate("agents/cts-worker.md");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load the bundled persona-injection hook script template.
|
|
61
|
+
*/
|
|
62
|
+
export function loadHookTemplate() {
|
|
63
|
+
return loadTemplate("hooks/cts-worker-persona.sh");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Install (or refuse to overwrite) one template file at a target path.
|
|
67
|
+
* Returns true if a write happened.
|
|
68
|
+
*/
|
|
69
|
+
function installTemplateFile(templateContent, targetPath, overwrite, description) {
|
|
70
|
+
const exists = existsSync(targetPath);
|
|
71
|
+
if (exists && !overwrite) {
|
|
72
|
+
const onDisk = readFileSync(targetPath, "utf-8");
|
|
73
|
+
if (onDisk === templateContent) {
|
|
74
|
+
return { installed: false, overwrote: false };
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`${description} already exists at ${targetPath} and differs from the bundled template. Pass --overwrite to replace it, or delete the file first.`);
|
|
77
|
+
}
|
|
78
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
79
|
+
writeFileSync(targetPath, templateContent, "utf-8");
|
|
80
|
+
return { installed: true, overwrote: exists };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Register the cts-worker-persona PreToolUse hook in the project's
|
|
84
|
+
* `.claude/settings.local.json`, preserving any existing keys.
|
|
85
|
+
*
|
|
86
|
+
* Returns true if the hook registration was added or updated.
|
|
87
|
+
*/
|
|
88
|
+
function registerHookInSettings(projectRoot, hookScriptPath) {
|
|
89
|
+
const settingsPath = join(projectRoot, ".claude", "settings.local.json");
|
|
90
|
+
const hookEntry = {
|
|
91
|
+
matcher: "Task",
|
|
92
|
+
hooks: [{ type: "command", command: hookScriptPath }],
|
|
93
|
+
};
|
|
94
|
+
// Read existing settings if present.
|
|
95
|
+
let settings = {};
|
|
96
|
+
if (existsSync(settingsPath)) {
|
|
97
|
+
try {
|
|
98
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
99
|
+
if (content.trim()) {
|
|
100
|
+
settings = JSON.parse(content);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Invalid JSON — refuse to clobber. Tell caller to fix manually.
|
|
105
|
+
throw new Error(`Existing ${settingsPath} contains invalid JSON. Fix it or delete it before installing the orchestrator hook.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Initialize the hooks tree if missing.
|
|
109
|
+
// biome-ignore lint/suspicious/noExplicitAny: settings.local.json is user-owned; we narrow ad-hoc.
|
|
110
|
+
const hooks = (settings.hooks ?? {});
|
|
111
|
+
const preToolUse = (hooks.PreToolUse ?? []);
|
|
112
|
+
// Check if an entry already references our hook script (idempotent).
|
|
113
|
+
const alreadyRegistered = preToolUse.some((entry) => {
|
|
114
|
+
if (entry.matcher !== "Task")
|
|
115
|
+
return false;
|
|
116
|
+
const innerHooks = entry.hooks;
|
|
117
|
+
if (!Array.isArray(innerHooks))
|
|
118
|
+
return false;
|
|
119
|
+
return innerHooks.some((h) => h.command === hookScriptPath);
|
|
120
|
+
});
|
|
121
|
+
if (alreadyRegistered) {
|
|
122
|
+
return { settingsPath, registered: false };
|
|
123
|
+
}
|
|
124
|
+
preToolUse.push(hookEntry);
|
|
125
|
+
hooks.PreToolUse = preToolUse;
|
|
126
|
+
settings.hooks = hooks;
|
|
127
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
128
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
129
|
+
return { settingsPath, registered: true };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Install (or refuse to overwrite) the codebase-to-spec skill bundle.
|
|
133
|
+
*
|
|
134
|
+
* For `project` scope: installs skill body, cts-worker agent, persona hook
|
|
135
|
+
* script, and registers the hook in settings.local.json.
|
|
136
|
+
*
|
|
137
|
+
* For `global` and `custom` scopes: installs only the skill body.
|
|
54
138
|
*/
|
|
55
139
|
export function installSkill(options = {}) {
|
|
56
140
|
const skillDir = resolveSkillDir(options);
|
|
57
141
|
const skillPath = join(skillDir, "SKILL.md");
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
throw new Error(`SKILL.md already exists at ${skillPath} and differs from the bundled template. Pass --overwrite to replace it, or delete the file first.`);
|
|
142
|
+
const skillTemplate = loadSkillTemplate();
|
|
143
|
+
const skillResult = installTemplateFile(skillTemplate, skillPath, options.overwrite === true, "SKILL.md");
|
|
144
|
+
// Skill body only for global/custom scopes — CC reads agents and hooks
|
|
145
|
+
// per-project, so the companions don't make sense outside a project.
|
|
146
|
+
const scope = options.scope ?? "project";
|
|
147
|
+
if (scope !== "project") {
|
|
148
|
+
return {
|
|
149
|
+
installedPath: skillPath,
|
|
150
|
+
installed: skillResult.installed,
|
|
151
|
+
overwrote: skillResult.overwrote,
|
|
152
|
+
};
|
|
70
153
|
}
|
|
71
|
-
|
|
72
|
-
|
|
154
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
155
|
+
const agentPath = join(projectRoot, ".claude", "agents", "cts-worker.md");
|
|
156
|
+
const hookScriptPath = join(projectRoot, ".claude", "hooks", "cts-worker-persona.sh");
|
|
157
|
+
// Install the cts-worker agent definition.
|
|
158
|
+
installTemplateFile(loadAgentTemplate(), agentPath, options.overwrite === true, "cts-worker agent definition");
|
|
159
|
+
// Install the persona-injection hook script (executable bit set separately).
|
|
160
|
+
installTemplateFile(loadHookTemplate(), hookScriptPath, options.overwrite === true, "cts-worker-persona hook script");
|
|
161
|
+
// chmod the hook script to be executable so Claude Code's PreToolUse
|
|
162
|
+
// hook can invoke it. (Imported statically — the CLI package is ESM, so
|
|
163
|
+
// `require("node:fs")` here would throw ReferenceError and silently
|
|
164
|
+
// leave the script at 0644, breaking the hook on every install.)
|
|
165
|
+
chmodSync(hookScriptPath, 0o755);
|
|
166
|
+
// Register the hook in settings.local.json.
|
|
167
|
+
const { settingsPath, registered } = registerHookInSettings(projectRoot, hookScriptPath);
|
|
73
168
|
return {
|
|
74
169
|
installedPath: skillPath,
|
|
75
|
-
installed:
|
|
76
|
-
overwrote:
|
|
170
|
+
installed: skillResult.installed,
|
|
171
|
+
overwrote: skillResult.overwrote,
|
|
172
|
+
companions: {
|
|
173
|
+
agentPath,
|
|
174
|
+
hookScriptPath,
|
|
175
|
+
settingsPath,
|
|
176
|
+
hookRegistered: registered,
|
|
177
|
+
},
|
|
77
178
|
};
|
|
78
179
|
}
|
|
79
180
|
//# sourceMappingURL=skill-install.js.map
|
|
@@ -24,6 +24,12 @@ function buildUserMessage(ctx) {
|
|
|
24
24
|
`Area description: ${ctx.area.description}`,
|
|
25
25
|
`Document defaultPrefix: ${ctx.outline.defaultPrefix}`,
|
|
26
26
|
`Area prefix: ${ctx.area.prefix}`,
|
|
27
|
+
``,
|
|
28
|
+
`## Customers this area serves`,
|
|
29
|
+
`(Pick one of these as the named persona for your requirements. Do not invent a different customer. If none of these is a real user of the software — e.g., all entries describe a contributor to this codebase — follow the AREA-LACKS-CUSTOMER escape in your system prompt instead of writing requirements.)`,
|
|
30
|
+
`\`\`\`json`,
|
|
31
|
+
JSON.stringify(ctx.area.customers, null, 2),
|
|
32
|
+
`\`\`\``,
|
|
27
33
|
`Use full requirement IDs of the form: ${ctx.outline.defaultPrefix}-${ctx.area.prefix}-1, ${ctx.outline.defaultPrefix}-${ctx.area.prefix}-2, etc. (sequential, 1-indexed, no zero-padding).`,
|
|
28
34
|
``,
|
|
29
35
|
`## Paths`,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotrequirements codebase-to-spec compose-orchestrator` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Conversational-orchestrator variant of `compose`. Reads outline.yaml
|
|
5
|
+
* (must be approved) + per-area partials, writes the composed spec to
|
|
6
|
+
* .dotrequirements-cache/spec-composed.md. Wraps the legacy `runCompose`
|
|
7
|
+
* by adapting outline.yaml to the legacy Outline shape.
|
|
8
|
+
*
|
|
9
|
+
* Requirements covered:
|
|
10
|
+
* - CTS-COMPOSE-1, CTS-COMPOSE-2 (reuses the legacy compose logic)
|
|
11
|
+
* - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
|
|
12
|
+
*/
|
|
13
|
+
export declare function composeOrchestratorCommand(): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=compose-orchestrator.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotrequirements codebase-to-spec compose-orchestrator` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Conversational-orchestrator variant of `compose`. Reads outline.yaml
|
|
5
|
+
* (must be approved) + per-area partials, writes the composed spec to
|
|
6
|
+
* .dotrequirements-cache/spec-composed.md. Wraps the legacy `runCompose`
|
|
7
|
+
* by adapting outline.yaml to the legacy Outline shape.
|
|
8
|
+
*
|
|
9
|
+
* Requirements covered:
|
|
10
|
+
* - CTS-COMPOSE-1, CTS-COMPOSE-2 (reuses the legacy compose logic)
|
|
11
|
+
* - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { cachePaths } from "../../codebase-to-spec/cache.js";
|
|
15
|
+
import { runCompose } from "../../codebase-to-spec/compose.js";
|
|
16
|
+
import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
|
|
17
|
+
import { conversationalOutlineToLegacy, parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
|
|
18
|
+
import { findProjectRoot } from "../../utils/project-settings.js";
|
|
19
|
+
export async function composeOrchestratorCommand() {
|
|
20
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
21
|
+
const paths = cachePaths(projectRoot);
|
|
22
|
+
if (!existsSync(paths.outline)) {
|
|
23
|
+
process.stderr.write(`No outline.yaml found at ${paths.outline}. Run the orchestrator's planner first.\n`);
|
|
24
|
+
process.exitCode = ExitCode.MissingInput;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let outline;
|
|
28
|
+
try {
|
|
29
|
+
outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
33
|
+
process.exitCode = ExitCode.MissingInput;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!outline.review || outline.review.result !== "approved") {
|
|
37
|
+
process.stderr.write(`Outline at ${paths.outline} is not approved (review.result is "${outline.review?.result ?? "absent"}"). Approve the outline before composing.\n`);
|
|
38
|
+
process.exitCode = ExitCode.MissingInput;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const legacyOutline = conversationalOutlineToLegacy(outline);
|
|
42
|
+
const result = runCompose({
|
|
43
|
+
outline: legacyOutline,
|
|
44
|
+
partialPathFor: (sanitized) => paths.partial(sanitized),
|
|
45
|
+
composedPath: paths.composedSpec,
|
|
46
|
+
});
|
|
47
|
+
process.stdout.write(`Composed spec → ${result.composedPath} (${result.partialsIncluded} partials included, ${result.partialsMissing} missing)\n`);
|
|
48
|
+
if (result.validationError) {
|
|
49
|
+
process.stderr.write(`Composed spec failed validation:\n${result.validationError}\n`);
|
|
50
|
+
process.exitCode = ExitCode.StageFailed;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=compose-orchestrator.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotrequirements codebase-to-spec dispatch-context <dispatch-id>` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Returns the composed prompt for a worker dispatch as a JSON object on stdout.
|
|
5
|
+
* Called by the cts-worker PreToolUse hook to compose the subagent's first-turn
|
|
6
|
+
* prompt via `modifiedInput`.
|
|
7
|
+
*
|
|
8
|
+
* Requirements covered:
|
|
9
|
+
* - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
|
|
10
|
+
*/
|
|
11
|
+
export declare function dispatchContextCommand(dispatchId: string): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=dispatch-context.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotrequirements codebase-to-spec dispatch-context <dispatch-id>` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Returns the composed prompt for a worker dispatch as a JSON object on stdout.
|
|
5
|
+
* Called by the cts-worker PreToolUse hook to compose the subagent's first-turn
|
|
6
|
+
* prompt via `modifiedInput`.
|
|
7
|
+
*
|
|
8
|
+
* Requirements covered:
|
|
9
|
+
* - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
|
|
10
|
+
*/
|
|
11
|
+
import { composeDispatchContext } from "../../codebase-to-spec/dispatch.js";
|
|
12
|
+
import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
|
|
13
|
+
export async function dispatchContextCommand(dispatchId) {
|
|
14
|
+
const ctx = composeDispatchContext(dispatchId);
|
|
15
|
+
if (!ctx) {
|
|
16
|
+
process.stderr.write(`Unknown dispatch-id: ${dispatchId}\n`);
|
|
17
|
+
process.exitCode = ExitCode.MissingInput;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
process.stdout.write(`${JSON.stringify(ctx)}\n`);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=dispatch-context.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotrequirements codebase-to-spec dispatch-editor <area-prefix>` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Returns a dispatch payload for an editor pass on a specific area's partial.
|
|
5
|
+
* Validates that the area's `review.result` is `"needs-revision"` and that
|
|
6
|
+
* the partial file exists before scaffolding the dispatch.
|
|
7
|
+
*
|
|
8
|
+
* The worker overwrites the existing partial via Edit; the area.review.thread
|
|
9
|
+
* in outline.yaml stays where it is (the orchestrator manages it).
|
|
10
|
+
*
|
|
11
|
+
* Requirements covered:
|
|
12
|
+
* - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
|
|
13
|
+
* - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
|
|
14
|
+
*/
|
|
15
|
+
export declare function dispatchEditorCommand(areaPrefix: string): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=dispatch-editor.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dotrequirements codebase-to-spec dispatch-editor <area-prefix>` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Returns a dispatch payload for an editor pass on a specific area's partial.
|
|
5
|
+
* Validates that the area's `review.result` is `"needs-revision"` and that
|
|
6
|
+
* the partial file exists before scaffolding the dispatch.
|
|
7
|
+
*
|
|
8
|
+
* The worker overwrites the existing partial via Edit; the area.review.thread
|
|
9
|
+
* in outline.yaml stays where it is (the orchestrator manages it).
|
|
10
|
+
*
|
|
11
|
+
* Requirements covered:
|
|
12
|
+
* - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
|
|
13
|
+
* - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { cachePaths } from "../../codebase-to-spec/cache.js";
|
|
17
|
+
import { EDITOR_DISPATCH_ID_PREFIX } from "../../codebase-to-spec/dispatch.js";
|
|
18
|
+
import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
|
|
19
|
+
import { sanitizeAreaName } from "../../codebase-to-spec/fan-out.js";
|
|
20
|
+
import { parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
|
|
21
|
+
import { findProjectRoot } from "../../utils/project-settings.js";
|
|
22
|
+
export async function dispatchEditorCommand(areaPrefix) {
|
|
23
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
24
|
+
const paths = cachePaths(projectRoot);
|
|
25
|
+
if (!existsSync(paths.outline)) {
|
|
26
|
+
process.stderr.write(`No outline.yaml found at ${paths.outline}. Dispatch the planner first.\n`);
|
|
27
|
+
process.exitCode = ExitCode.MissingInput;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let outline;
|
|
31
|
+
try {
|
|
32
|
+
outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
36
|
+
process.exitCode = ExitCode.MissingInput;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const area = outline.areas.find((a) => a.prefix === areaPrefix);
|
|
40
|
+
if (!area) {
|
|
41
|
+
process.stderr.write(`No area with prefix "${areaPrefix}" in outline.yaml. Available prefixes: ${outline.areas
|
|
42
|
+
.map((a) => a.prefix)
|
|
43
|
+
.join(", ")}\n`);
|
|
44
|
+
process.exitCode = ExitCode.MissingInput;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!area.review) {
|
|
48
|
+
process.stderr.write(`Area "${areaPrefix}" has no review yet. Write a per-area review with result=needs-revision before dispatching the editor.\n`);
|
|
49
|
+
process.exitCode = ExitCode.MissingInput;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (area.review.result === "approved") {
|
|
53
|
+
process.stderr.write(`Area "${areaPrefix}" has review.result "approved" — nothing to revise. The partial is good as-is.\n`);
|
|
54
|
+
process.exitCode = ExitCode.MissingInput;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const partialPath = paths.partial(sanitizeAreaName(area.name));
|
|
58
|
+
if (!existsSync(partialPath)) {
|
|
59
|
+
process.stderr.write(`Partial not found at ${partialPath}. Run the specifier for area "${areaPrefix}" before dispatching the editor.\n`);
|
|
60
|
+
process.exitCode = ExitCode.MissingInput;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const payload = {
|
|
64
|
+
dispatch_id: `${EDITOR_DISPATCH_ID_PREFIX}${areaPrefix}`,
|
|
65
|
+
output_path: partialPath,
|
|
66
|
+
area_name: area.name,
|
|
67
|
+
area_prefix: area.prefix,
|
|
68
|
+
};
|
|
69
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=dispatch-editor.js.map
|