@structured-vibe-coding/core 0.17.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/defaults/prices.yaml +9 -0
- package/defaults/search.yaml +18 -0
- package/defaults/teams/research/roles/researcher.md +29 -0
- package/defaults/teams/research/team.yaml +7 -0
- package/defaults/teams/software/roles/architect.md +31 -0
- package/defaults/teams/software/roles/dev-lead.md +32 -0
- package/defaults/teams/software/roles/developer.md +30 -0
- package/defaults/teams/software/roles/product-manager.md +32 -0
- package/defaults/teams/software/roles/product-owner.md +30 -0
- package/defaults/teams/software/roles/qa.md +35 -0
- package/defaults/teams/software/routing.yaml +28 -0
- package/defaults/teams/software/team.yaml +24 -0
- package/defaults/teams/solo/roles/developer.md +28 -0
- package/defaults/teams/solo/team.yaml +7 -0
- package/defaults/templates/plan.md +17 -0
- package/defaults/templates/review.md +11 -0
- package/defaults/templates/story.md +13 -0
- package/defaults/templates/task.md +11 -0
- package/defaults/workflows/team-software.yaml +153 -0
- package/dist/{agents-RWYBLZQE.js → agents-PSIOQEYH.js} +18 -7
- package/dist/{change-context-W4HDO2IK.js → change-context-EZJMSGSP.js} +2 -4
- package/dist/chunk-26YG6A4M.js +1210 -0
- package/dist/chunk-2FJBFYVJ.js +1576 -0
- package/dist/{chunk-KYWETNFU.js → chunk-4BRO7BOJ.js} +2 -4
- package/dist/{chunk-74OGSVJZ.js → chunk-H55RFAEI.js} +2 -2
- package/dist/chunk-HG7TPW5V.js +567 -0
- package/dist/{chunk-EPFURATC.js → chunk-O5SP6X5R.js} +89 -0
- package/dist/chunk-TGRV6WGS.js +3436 -0
- package/dist/{gather-BZDW4EN4.js → gather-7TSVKQO2.js} +3 -4
- package/dist/{greenfield-Z425JOMP.js → greenfield-QFA5WVHE.js} +5 -6
- package/dist/index.d.ts +1832 -44
- package/dist/index.js +5172 -992
- package/dist/{sources-PF45EUH2.js → sources-3TXUPU6V.js} +2 -3
- package/package.json +11 -3
- package/vendor/tree-sitter/NOTICE +29 -0
- package/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/dist/chunk-67AUAGNV.js +0 -150
- package/dist/chunk-7JLDL2OK.js +0 -771
- package/dist/chunk-7MJYPJMR.js +0 -51
- package/dist/chunk-A752UVPF.js +0 -317
- package/dist/chunk-P3N67UEZ.js +0 -538
- package/dist/chunk-R5WQN4HA.js +0 -139
- package/dist/chunk-WHDG6OTG.js +0 -46
- package/dist/config-X5VDBVR4.js +0 -16
- package/dist/fs-J6DIV6CX.js +0 -28
- package/dist/paths-64GA32IZ.js +0 -28
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
interpolate,
|
|
3
3
|
interpolateArgs,
|
|
4
4
|
loadMcpConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4BRO7BOJ.js";
|
|
6
6
|
import {
|
|
7
7
|
SddError
|
|
8
8
|
} from "./chunk-MHODNFIL.js";
|
|
@@ -26,7 +26,7 @@ function toProcessEnv(base, extra) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// src/version.ts
|
|
29
|
-
var SDD_CORE_VERSION = "0.
|
|
29
|
+
var SDD_CORE_VERSION = "0.19.0" ? "0.19.0" : "0.0.0-dev";
|
|
30
30
|
|
|
31
31
|
// src/mcp/client.ts
|
|
32
32
|
function mcpTimeoutMs() {
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activePointerPath,
|
|
3
|
+
changeMetaPath,
|
|
4
|
+
changePath,
|
|
5
|
+
changesDir,
|
|
6
|
+
displayChangePath,
|
|
7
|
+
ensureDir,
|
|
8
|
+
listDirs,
|
|
9
|
+
listFiles,
|
|
10
|
+
pathExists,
|
|
11
|
+
readText,
|
|
12
|
+
readYaml,
|
|
13
|
+
workflowsDir,
|
|
14
|
+
writeText,
|
|
15
|
+
writeYaml
|
|
16
|
+
} from "./chunk-O5SP6X5R.js";
|
|
17
|
+
|
|
18
|
+
// src/schemas.ts
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
var GateSchema = z.object({
|
|
21
|
+
type: z.enum(["none", "soft", "hard"]).default("soft"),
|
|
22
|
+
mode: z.enum(["none", "human_approval", "checklist"]).default("checklist"),
|
|
23
|
+
checklist: z.array(z.string()).default([]),
|
|
24
|
+
/** Role names, or human for a person listed on the team. */
|
|
25
|
+
approvers: z.array(z.string().trim().min(1)).optional(),
|
|
26
|
+
overridable: z.boolean().default(true)
|
|
27
|
+
});
|
|
28
|
+
var ArtifactDefSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
path: z.string(),
|
|
31
|
+
template: z.string().optional(),
|
|
32
|
+
required: z.boolean().default(true)
|
|
33
|
+
});
|
|
34
|
+
var VerifyCommandSchema = z.object({
|
|
35
|
+
name: z.string(),
|
|
36
|
+
run: z.string(),
|
|
37
|
+
required: z.boolean().default(false)
|
|
38
|
+
});
|
|
39
|
+
var StageSchema = z.object({
|
|
40
|
+
id: z.string(),
|
|
41
|
+
title: z.string().optional(),
|
|
42
|
+
summary: z.string().optional(),
|
|
43
|
+
/**
|
|
44
|
+
* standard — normal work stage
|
|
45
|
+
* brainstorm — optional multi-approach exploration (before design)
|
|
46
|
+
* clarify — optional Q&A before leaving a major stage
|
|
47
|
+
*/
|
|
48
|
+
kind: z.enum(["standard", "brainstorm", "clarify"]).default("standard"),
|
|
49
|
+
/** When true, stage is optional (skippable); agents may skip if not needed */
|
|
50
|
+
optional: z.boolean().default(false),
|
|
51
|
+
/** Role that produces this stage. */
|
|
52
|
+
owner: z.string().trim().min(1).optional(),
|
|
53
|
+
/** Roles who work with the owner on this stage. */
|
|
54
|
+
collaborates: z.array(z.string().trim().min(1)).optional(),
|
|
55
|
+
/**
|
|
56
|
+
* One item per matching file when the stage starts.
|
|
57
|
+
* A directory, a slash, and one star in the file name: tasks/T-*.md
|
|
58
|
+
*/
|
|
59
|
+
items_from: z.string().regex(/^(?!.*\.\.)[A-Za-z0-9._-]+\/[A-Za-z0-9._-]*\*[A-Za-z0-9._-]*$/).optional(),
|
|
60
|
+
artifacts: z.array(ArtifactDefSchema).default([]),
|
|
61
|
+
gate: GateSchema.default({}),
|
|
62
|
+
skippable: z.boolean().default(true),
|
|
63
|
+
skip_when: z.object({
|
|
64
|
+
flags: z.array(z.string()).optional()
|
|
65
|
+
}).optional(),
|
|
66
|
+
verify: z.object({
|
|
67
|
+
commands: z.array(VerifyCommandSchema).default([]),
|
|
68
|
+
evidence_dir: z.string().optional()
|
|
69
|
+
}).optional(),
|
|
70
|
+
agent_context: z.object({
|
|
71
|
+
include: z.array(z.string()).default([]),
|
|
72
|
+
instructions: z.string().optional()
|
|
73
|
+
}).optional()
|
|
74
|
+
});
|
|
75
|
+
var WorkflowSchema = z.object({
|
|
76
|
+
name: z.string(),
|
|
77
|
+
description: z.string().optional(),
|
|
78
|
+
version: z.number().default(1),
|
|
79
|
+
stages: z.array(StageSchema).min(1),
|
|
80
|
+
recommendation: z.object({
|
|
81
|
+
when: z.object({
|
|
82
|
+
labels: z.array(z.string()).optional(),
|
|
83
|
+
keywords: z.array(z.string()).optional(),
|
|
84
|
+
max_complexity: z.enum(["low", "medium", "high"]).optional(),
|
|
85
|
+
min_complexity: z.enum(["low", "medium", "high"]).optional()
|
|
86
|
+
}).optional(),
|
|
87
|
+
priority: z.number().default(0)
|
|
88
|
+
}).optional(),
|
|
89
|
+
on_complete: z.object({
|
|
90
|
+
archive: z.boolean().default(false),
|
|
91
|
+
domain_sync: z.enum(["never", "recommend", "require"]).default("never")
|
|
92
|
+
}).optional()
|
|
93
|
+
});
|
|
94
|
+
var ConfigSchema = z.object({
|
|
95
|
+
version: z.number().default(1),
|
|
96
|
+
metrics: z.object({
|
|
97
|
+
enabled: z.boolean().default(true),
|
|
98
|
+
exclude: z.array(z.string().min(1)).default([])
|
|
99
|
+
}).default({}),
|
|
100
|
+
memory_path: z.string().default("memory"),
|
|
101
|
+
changes_path: z.string().default("changes"),
|
|
102
|
+
archive_path: z.string().default("archive"),
|
|
103
|
+
domains_path: z.string().default("domains"),
|
|
104
|
+
default_workflow: z.string().default("recommend"),
|
|
105
|
+
allowed_workflows: z.array(z.string()).optional(),
|
|
106
|
+
per_change: z.object({
|
|
107
|
+
allow_skip: z.boolean().default(true),
|
|
108
|
+
allow_gate_override: z.enum(["true", "false", "soft_only"]).default("soft_only"),
|
|
109
|
+
allow_custom_stages: z.boolean().default(true),
|
|
110
|
+
require_reason_on_skip: z.boolean().default(true)
|
|
111
|
+
}).default({}),
|
|
112
|
+
persistence: z.object({
|
|
113
|
+
default: z.enum(["change_only", "domain_sync"]).default("change_only"),
|
|
114
|
+
archive_on_complete: z.boolean().default(false),
|
|
115
|
+
domain_sync: z.object({
|
|
116
|
+
mode: z.enum(["never", "recommend", "require"]).default("never"),
|
|
117
|
+
anchored_domains: z.array(z.string()).default([])
|
|
118
|
+
}).default({})
|
|
119
|
+
}).default({}),
|
|
120
|
+
policy: z.object({
|
|
121
|
+
gates: z.enum(["soft", "hard"]).default("soft")
|
|
122
|
+
}).default({})
|
|
123
|
+
});
|
|
124
|
+
var ChangeMetaSchema = z.object({
|
|
125
|
+
id: z.string(),
|
|
126
|
+
title: z.string(),
|
|
127
|
+
workflow: z.string(),
|
|
128
|
+
created: z.string(),
|
|
129
|
+
updated: z.string().optional(),
|
|
130
|
+
status: z.enum(["in_progress", "blocked", "completed", "abandoned"]).default("in_progress"),
|
|
131
|
+
stage: z.string(),
|
|
132
|
+
branch: z.string().optional(),
|
|
133
|
+
/** Epic key such as issue-37. Stories in one epic share a dependency graph. */
|
|
134
|
+
epic: z.string().optional(),
|
|
135
|
+
/** Story id such as S-001. */
|
|
136
|
+
story: z.string().optional(),
|
|
137
|
+
depends: z.array(z.string()).optional(),
|
|
138
|
+
domain: z.string().optional(),
|
|
139
|
+
flags: z.record(z.union([z.boolean(), z.string(), z.number()])).default({}),
|
|
140
|
+
overrides: z.object({
|
|
141
|
+
skip_stages: z.array(z.string()).default([]),
|
|
142
|
+
gates: z.record(z.enum(["none", "soft", "hard"])).default({}),
|
|
143
|
+
extra_stages: z.array(StageSchema).default([])
|
|
144
|
+
}).default({}),
|
|
145
|
+
skipped: z.array(
|
|
146
|
+
z.object({
|
|
147
|
+
stage: z.string(),
|
|
148
|
+
reason: z.string(),
|
|
149
|
+
at: z.string()
|
|
150
|
+
})
|
|
151
|
+
).default([]),
|
|
152
|
+
gates: z.record(
|
|
153
|
+
z.object({
|
|
154
|
+
status: z.enum(["pending", "approved", "failed", "waived"]),
|
|
155
|
+
note: z.string().optional(),
|
|
156
|
+
at: z.string().optional()
|
|
157
|
+
})
|
|
158
|
+
).default({}),
|
|
159
|
+
/** Results from `sdd verify` keyed by stage id */
|
|
160
|
+
verify_results: z.record(
|
|
161
|
+
z.object({
|
|
162
|
+
ok: z.boolean(),
|
|
163
|
+
/** pass | fail | not_run (absent in metas written before 0.17.3) */
|
|
164
|
+
status: z.enum(["pass", "fail", "not_run"]).optional(),
|
|
165
|
+
head_sha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i).optional(),
|
|
166
|
+
at: z.string(),
|
|
167
|
+
results: z.array(
|
|
168
|
+
z.object({
|
|
169
|
+
name: z.string(),
|
|
170
|
+
exitCode: z.number().nullable()
|
|
171
|
+
})
|
|
172
|
+
).default([])
|
|
173
|
+
})
|
|
174
|
+
).default({}),
|
|
175
|
+
pr: z.object({
|
|
176
|
+
number: z.number().optional(),
|
|
177
|
+
url: z.string().optional()
|
|
178
|
+
}).optional(),
|
|
179
|
+
completed_at: z.string().optional()
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// src/slug.ts
|
|
183
|
+
var SLUG_MAX = 60;
|
|
184
|
+
function trailingIssueRef(input) {
|
|
185
|
+
return input.match(/#\s*(\d+)\s*\)?\s*$/)?.[1];
|
|
186
|
+
}
|
|
187
|
+
function slugify(input) {
|
|
188
|
+
const normalized = input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
189
|
+
if (normalized.length <= SLUG_MAX) return normalized;
|
|
190
|
+
const issue = trailingIssueRef(input);
|
|
191
|
+
const tokens = normalized.split("-").filter(Boolean);
|
|
192
|
+
const body = issue && tokens.at(-1) === issue ? tokens.slice(0, -1) : tokens;
|
|
193
|
+
const reserve = issue && issue.length <= SLUG_MAX ? issue.length + 1 : 0;
|
|
194
|
+
const kept = [];
|
|
195
|
+
let length = 0;
|
|
196
|
+
for (const token of body) {
|
|
197
|
+
const next = length === 0 ? token.length : length + 1 + token.length;
|
|
198
|
+
if (next + reserve > SLUG_MAX) break;
|
|
199
|
+
kept.push(token);
|
|
200
|
+
length = next;
|
|
201
|
+
}
|
|
202
|
+
if (reserve && issue) {
|
|
203
|
+
if (kept.length === 0) return issue;
|
|
204
|
+
return `${kept.join("-")}-${issue}`;
|
|
205
|
+
}
|
|
206
|
+
return kept.join("-");
|
|
207
|
+
}
|
|
208
|
+
function todayIsoDate(d = /* @__PURE__ */ new Date()) {
|
|
209
|
+
return d.toISOString().slice(0, 10);
|
|
210
|
+
}
|
|
211
|
+
function nowIso(d = /* @__PURE__ */ new Date()) {
|
|
212
|
+
return d.toISOString();
|
|
213
|
+
}
|
|
214
|
+
function changeDirName(title, d = /* @__PURE__ */ new Date()) {
|
|
215
|
+
const slug = slugify(title) || "change";
|
|
216
|
+
return `${todayIsoDate(d)}-${slug}`;
|
|
217
|
+
}
|
|
218
|
+
function uniqueChangeDirName(base, taken) {
|
|
219
|
+
if (!taken.has(base)) return base;
|
|
220
|
+
let n = 2;
|
|
221
|
+
while (taken.has(`${base}-${n}`)) n += 1;
|
|
222
|
+
return `${base}-${n}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/workflow.ts
|
|
226
|
+
import { join as join2 } from "pathe";
|
|
227
|
+
|
|
228
|
+
// src/defaults.ts
|
|
229
|
+
import { existsSync } from "fs";
|
|
230
|
+
import { fileURLToPath } from "url";
|
|
231
|
+
import { dirname, join } from "pathe";
|
|
232
|
+
function defaultsRoot() {
|
|
233
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
234
|
+
const candidates = [
|
|
235
|
+
join(here, "..", "defaults"),
|
|
236
|
+
// npm package: dist/../defaults
|
|
237
|
+
join(here, "defaults"),
|
|
238
|
+
// same-dir defaults if present
|
|
239
|
+
join(here, "..", "..", "defaults")
|
|
240
|
+
// src/ during some test runners
|
|
241
|
+
];
|
|
242
|
+
for (const c of candidates) {
|
|
243
|
+
if (existsSync(c)) return c;
|
|
244
|
+
}
|
|
245
|
+
return candidates[0];
|
|
246
|
+
}
|
|
247
|
+
function defaultWorkflowsDir() {
|
|
248
|
+
return join(defaultsRoot(), "workflows");
|
|
249
|
+
}
|
|
250
|
+
function defaultTeamsDir() {
|
|
251
|
+
return join(defaultsRoot(), "teams");
|
|
252
|
+
}
|
|
253
|
+
function defaultTemplatesDir() {
|
|
254
|
+
return join(defaultsRoot(), "templates");
|
|
255
|
+
}
|
|
256
|
+
function defaultMemoryDir() {
|
|
257
|
+
return join(defaultsRoot(), "memory");
|
|
258
|
+
}
|
|
259
|
+
function defaultMcpYamlPath() {
|
|
260
|
+
return join(defaultsRoot(), "mcp.yaml");
|
|
261
|
+
}
|
|
262
|
+
function defaultPricesYamlPath() {
|
|
263
|
+
return join(defaultsRoot(), "prices.yaml");
|
|
264
|
+
}
|
|
265
|
+
function defaultSearchYamlPath() {
|
|
266
|
+
return join(defaultsRoot(), "search.yaml");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/workflow.ts
|
|
270
|
+
function workflowNamesIn(files) {
|
|
271
|
+
return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
|
|
272
|
+
}
|
|
273
|
+
async function listWorkflowNames(projectRoot) {
|
|
274
|
+
const dir = workflowsDir(projectRoot);
|
|
275
|
+
return workflowNamesIn(await listFiles(dir));
|
|
276
|
+
}
|
|
277
|
+
async function listBundledWorkflowNames() {
|
|
278
|
+
return workflowNamesIn(await listFiles(defaultWorkflowsDir()));
|
|
279
|
+
}
|
|
280
|
+
async function missingBundledWorkflows(projectRoot) {
|
|
281
|
+
const present = new Set(await listWorkflowNames(projectRoot));
|
|
282
|
+
return (await listBundledWorkflowNames()).filter((name) => !present.has(name));
|
|
283
|
+
}
|
|
284
|
+
function workflowUnavailableMessage(name, available, bundled) {
|
|
285
|
+
const listed = available.join(", ") || "(none)";
|
|
286
|
+
const base = `Workflow "${name}" is not available. Available: ${listed}`;
|
|
287
|
+
if (!bundled.includes(name)) return base;
|
|
288
|
+
return `${base}. This CLI ships ${name}. Run \`sdd init --here --force\` to copy new defaults.`;
|
|
289
|
+
}
|
|
290
|
+
async function loadWorkflow(projectRoot, name) {
|
|
291
|
+
const dir = workflowsDir(projectRoot);
|
|
292
|
+
const candidates = [`${name}.yaml`, `${name}.yml`];
|
|
293
|
+
for (const file of candidates) {
|
|
294
|
+
const path = join2(dir, file);
|
|
295
|
+
if (await pathExists(path)) {
|
|
296
|
+
const raw = await readYaml(path);
|
|
297
|
+
const workflow = WorkflowSchema.parse(raw);
|
|
298
|
+
if (workflow.name !== name) {
|
|
299
|
+
}
|
|
300
|
+
return workflow;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const available = await listWorkflowNames(projectRoot);
|
|
304
|
+
const bundled = await listBundledWorkflowNames();
|
|
305
|
+
const listed = available.join(", ") || "(none)";
|
|
306
|
+
let message = `Workflow "${name}" not found in ${dir}. Available: ${listed}`;
|
|
307
|
+
if (bundled.includes(name)) {
|
|
308
|
+
message += `. This CLI ships ${name}. Run \`sdd init --here --force\` to copy new defaults.`;
|
|
309
|
+
}
|
|
310
|
+
throw new Error(message);
|
|
311
|
+
}
|
|
312
|
+
function resolveStages(workflow, meta) {
|
|
313
|
+
const base = [...workflow.stages];
|
|
314
|
+
const extra = meta.overrides?.extra_stages ?? [];
|
|
315
|
+
const stages = [...base, ...extra];
|
|
316
|
+
return stages.map((stage) => {
|
|
317
|
+
const gateOverride = meta.overrides?.gates?.[stage.id];
|
|
318
|
+
if (!gateOverride) return stage;
|
|
319
|
+
return {
|
|
320
|
+
...stage,
|
|
321
|
+
gate: {
|
|
322
|
+
...stage.gate,
|
|
323
|
+
type: gateOverride
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function isStageSkipped(meta, stageId) {
|
|
329
|
+
if (meta.overrides?.skip_stages?.includes(stageId)) return true;
|
|
330
|
+
if (meta.skipped?.some((s) => s.stage === stageId)) return true;
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function shouldAutoSkip(stage, meta) {
|
|
334
|
+
const flags = stage.skip_when?.flags ?? [];
|
|
335
|
+
for (const flag of flags) {
|
|
336
|
+
if (meta.flags?.[flag] === true || meta.flags?.[flag] === "true") {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
function activeStages(workflow, meta) {
|
|
343
|
+
return resolveStages(workflow, meta).filter((s) => {
|
|
344
|
+
if (isStageSkipped(meta, s.id)) return false;
|
|
345
|
+
if (shouldAutoSkip(s, meta)) return false;
|
|
346
|
+
return true;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function getStage(workflow, meta, stageId) {
|
|
350
|
+
return resolveStages(workflow, meta).find((s) => s.id === stageId);
|
|
351
|
+
}
|
|
352
|
+
function nextStageId(workflow, meta) {
|
|
353
|
+
const all = resolveStages(workflow, meta);
|
|
354
|
+
const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
|
|
355
|
+
let start = all.findIndex((s) => s.id === meta.stage);
|
|
356
|
+
if (start === -1) start = -1;
|
|
357
|
+
for (let i = start + 1; i < all.length; i++) {
|
|
358
|
+
const id = all[i].id;
|
|
359
|
+
if (activeIds.has(id)) return id;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function firstActiveStageId(workflow, meta) {
|
|
364
|
+
return activeStages(workflow, meta)[0]?.id ?? null;
|
|
365
|
+
}
|
|
366
|
+
function inferComplexity(title, hints) {
|
|
367
|
+
const text = `${title} ${(hints?.keywords ?? []).join(" ")}`.toLowerCase();
|
|
368
|
+
const hotfixHints = ["typo", "hotfix", "nit", "rename", "bump", "chore", "readme", "docs"];
|
|
369
|
+
const highHints = [
|
|
370
|
+
"architecture",
|
|
371
|
+
"migration",
|
|
372
|
+
"auth",
|
|
373
|
+
"payment",
|
|
374
|
+
"billing",
|
|
375
|
+
"redesign",
|
|
376
|
+
"platform",
|
|
377
|
+
"arb",
|
|
378
|
+
"epic"
|
|
379
|
+
];
|
|
380
|
+
if (hotfixHints.some((h) => text.includes(h))) return "low";
|
|
381
|
+
if (highHints.some((h) => text.includes(h))) return "high";
|
|
382
|
+
if (text.length > 80) return "medium";
|
|
383
|
+
return "medium";
|
|
384
|
+
}
|
|
385
|
+
async function recommendWorkflow(projectRoot, title, config, opts) {
|
|
386
|
+
const available = await listWorkflowNames(projectRoot);
|
|
387
|
+
const allowed = config.allowed_workflows?.length ? available.filter((n) => config.allowed_workflows.includes(n)) : available;
|
|
388
|
+
if (opts?.preferred) {
|
|
389
|
+
if (!available.includes(opts.preferred)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
workflowUnavailableMessage(opts.preferred, available, await listBundledWorkflowNames())
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Workflow "${opts.preferred}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
name: opts.preferred,
|
|
401
|
+
reason: "Explicitly selected",
|
|
402
|
+
alternatives: allowed.filter((n) => n !== opts.preferred)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (config.default_workflow !== "recommend" && allowed.includes(config.default_workflow)) {
|
|
406
|
+
return {
|
|
407
|
+
name: config.default_workflow,
|
|
408
|
+
reason: `Repo default_workflow=${config.default_workflow}`,
|
|
409
|
+
alternatives: allowed.filter((n) => n !== config.default_workflow)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const complexity = opts?.complexity ?? inferComplexity(title);
|
|
413
|
+
const scored = [];
|
|
414
|
+
for (const name of allowed) {
|
|
415
|
+
try {
|
|
416
|
+
const wf = await loadWorkflow(projectRoot, name);
|
|
417
|
+
let score = wf.recommendation?.priority ?? 0;
|
|
418
|
+
let reason = `workflow priority ${score}`;
|
|
419
|
+
const when = wf.recommendation?.when;
|
|
420
|
+
if (complexity === "low" && name === "hotfix") {
|
|
421
|
+
score += 50;
|
|
422
|
+
reason = "Low complexity \u2192 hotfix";
|
|
423
|
+
} else if (complexity === "low" && name === "patch") {
|
|
424
|
+
score += 40;
|
|
425
|
+
reason = "Low complexity \u2192 patch";
|
|
426
|
+
} else if (complexity === "medium" && name === "feature") {
|
|
427
|
+
score += 45;
|
|
428
|
+
reason = "Medium complexity \u2192 feature";
|
|
429
|
+
} else if (complexity === "medium" && name === "patch") {
|
|
430
|
+
score += 30;
|
|
431
|
+
reason = "Medium complexity \u2192 patch";
|
|
432
|
+
} else if (complexity === "high" && name === "enterprise-feature") {
|
|
433
|
+
score += 50;
|
|
434
|
+
reason = "High complexity \u2192 enterprise-feature";
|
|
435
|
+
} else if (complexity === "high" && name === "feature") {
|
|
436
|
+
score += 35;
|
|
437
|
+
reason = "High complexity \u2192 feature";
|
|
438
|
+
}
|
|
439
|
+
const keywords = when?.keywords ?? [];
|
|
440
|
+
const titleLower = title.toLowerCase();
|
|
441
|
+
if (keywords.some((k) => titleLower.includes(k.toLowerCase()))) {
|
|
442
|
+
score += 20;
|
|
443
|
+
reason = `Keyword match for ${name}`;
|
|
444
|
+
}
|
|
445
|
+
scored.push({ name, score, reason });
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
scored.sort((a, b) => b.score - a.score);
|
|
450
|
+
const best = scored[0];
|
|
451
|
+
if (!best) {
|
|
452
|
+
throw new Error("No workflows available. Run `sdd init` or add files under .sdd/workflows/");
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
name: best.name,
|
|
456
|
+
reason: best.reason,
|
|
457
|
+
alternatives: scored.slice(1).map((s) => s.name)
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/change-context.ts
|
|
462
|
+
async function loadChangeMeta(projectRoot, config, changeId) {
|
|
463
|
+
const path = changeMetaPath(projectRoot, config, changeId);
|
|
464
|
+
if (!await pathExists(path)) {
|
|
465
|
+
throw new Error(`Change not found: ${changeId}`);
|
|
466
|
+
}
|
|
467
|
+
return ChangeMetaSchema.parse(await readYaml(path));
|
|
468
|
+
}
|
|
469
|
+
async function saveChangeMeta(projectRoot, config, meta) {
|
|
470
|
+
meta.updated = nowIso();
|
|
471
|
+
await writeYaml(changeMetaPath(projectRoot, config, meta.id), ChangeMetaSchema.parse(meta));
|
|
472
|
+
}
|
|
473
|
+
async function buildContext(projectRoot, config, changeId) {
|
|
474
|
+
const meta = await loadChangeMeta(projectRoot, config, changeId);
|
|
475
|
+
const workflow = await loadWorkflow(projectRoot, meta.workflow);
|
|
476
|
+
return {
|
|
477
|
+
id: changeId,
|
|
478
|
+
path: changePath(projectRoot, config, changeId),
|
|
479
|
+
displayPath: displayChangePath(config, changeId),
|
|
480
|
+
meta,
|
|
481
|
+
workflow,
|
|
482
|
+
stages: resolveStages(workflow, meta),
|
|
483
|
+
active: activeStages(workflow, meta)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async function listChanges(projectRoot, config) {
|
|
487
|
+
return listDirs(changesDir(projectRoot, config));
|
|
488
|
+
}
|
|
489
|
+
async function getActiveChangeId(projectRoot, config) {
|
|
490
|
+
const pointer = activePointerPath(projectRoot, config);
|
|
491
|
+
if (await pathExists(pointer)) {
|
|
492
|
+
const id = (await readText(pointer)).trim();
|
|
493
|
+
if (id && await pathExists(changePath(projectRoot, config, id))) {
|
|
494
|
+
return id;
|
|
495
|
+
}
|
|
496
|
+
if (id) {
|
|
497
|
+
await writeText(pointer, "");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
async function clearActiveIf(projectRoot, config, changeId) {
|
|
503
|
+
const pointer = activePointerPath(projectRoot, config);
|
|
504
|
+
if (!await pathExists(pointer)) return;
|
|
505
|
+
const id = (await readText(pointer)).trim();
|
|
506
|
+
if (id === changeId) {
|
|
507
|
+
await writeText(pointer, "");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function setActiveChange(projectRoot, config, changeId) {
|
|
511
|
+
await ensureDir(changesDir(projectRoot, config));
|
|
512
|
+
await writeText(activePointerPath(projectRoot, config), `${changeId}
|
|
513
|
+
`);
|
|
514
|
+
}
|
|
515
|
+
async function resolveChangeId(projectRoot, config, changeId) {
|
|
516
|
+
if (changeId) return changeId;
|
|
517
|
+
const active = await getActiveChangeId(projectRoot, config);
|
|
518
|
+
if (!active) {
|
|
519
|
+
throw new Error("No active change. Pass --change <id> or run `sdd new`.");
|
|
520
|
+
}
|
|
521
|
+
return active;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export {
|
|
525
|
+
GateSchema,
|
|
526
|
+
ArtifactDefSchema,
|
|
527
|
+
VerifyCommandSchema,
|
|
528
|
+
StageSchema,
|
|
529
|
+
WorkflowSchema,
|
|
530
|
+
ConfigSchema,
|
|
531
|
+
ChangeMetaSchema,
|
|
532
|
+
slugify,
|
|
533
|
+
todayIsoDate,
|
|
534
|
+
nowIso,
|
|
535
|
+
changeDirName,
|
|
536
|
+
uniqueChangeDirName,
|
|
537
|
+
defaultsRoot,
|
|
538
|
+
defaultWorkflowsDir,
|
|
539
|
+
defaultTeamsDir,
|
|
540
|
+
defaultTemplatesDir,
|
|
541
|
+
defaultMemoryDir,
|
|
542
|
+
defaultMcpYamlPath,
|
|
543
|
+
defaultPricesYamlPath,
|
|
544
|
+
defaultSearchYamlPath,
|
|
545
|
+
listWorkflowNames,
|
|
546
|
+
listBundledWorkflowNames,
|
|
547
|
+
missingBundledWorkflows,
|
|
548
|
+
workflowUnavailableMessage,
|
|
549
|
+
loadWorkflow,
|
|
550
|
+
resolveStages,
|
|
551
|
+
isStageSkipped,
|
|
552
|
+
shouldAutoSkip,
|
|
553
|
+
activeStages,
|
|
554
|
+
getStage,
|
|
555
|
+
nextStageId,
|
|
556
|
+
firstActiveStageId,
|
|
557
|
+
inferComplexity,
|
|
558
|
+
recommendWorkflow,
|
|
559
|
+
loadChangeMeta,
|
|
560
|
+
saveChangeMeta,
|
|
561
|
+
buildContext,
|
|
562
|
+
listChanges,
|
|
563
|
+
getActiveChangeId,
|
|
564
|
+
clearActiveIf,
|
|
565
|
+
setActiveChange,
|
|
566
|
+
resolveChangeId
|
|
567
|
+
};
|
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
// src/paths.ts
|
|
2
|
+
import { join } from "pathe";
|
|
3
|
+
var SDD_DIR = ".sdd";
|
|
4
|
+
function sddRoot(projectRoot) {
|
|
5
|
+
return join(projectRoot, SDD_DIR);
|
|
6
|
+
}
|
|
7
|
+
function configPath(projectRoot) {
|
|
8
|
+
return join(sddRoot(projectRoot), "config.yaml");
|
|
9
|
+
}
|
|
10
|
+
function workflowsDir(projectRoot) {
|
|
11
|
+
return join(sddRoot(projectRoot), "workflows");
|
|
12
|
+
}
|
|
13
|
+
function templatesDir(projectRoot) {
|
|
14
|
+
return join(sddRoot(projectRoot), "templates");
|
|
15
|
+
}
|
|
16
|
+
function memoryDir(projectRoot, config) {
|
|
17
|
+
return join(projectRoot, config.memory_path);
|
|
18
|
+
}
|
|
19
|
+
function stripTrailingSlashes(value) {
|
|
20
|
+
let end = value.length;
|
|
21
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
22
|
+
return value.slice(0, end);
|
|
23
|
+
}
|
|
24
|
+
function memoryPathRel(config) {
|
|
25
|
+
return stripTrailingSlashes(config.memory_path) || "memory";
|
|
26
|
+
}
|
|
27
|
+
function changesPathRel(config) {
|
|
28
|
+
return stripTrailingSlashes(config.changes_path) || "changes";
|
|
29
|
+
}
|
|
30
|
+
function displayArchivePath(config, changeId) {
|
|
31
|
+
return `${stripTrailingSlashes(config.archive_path) || "archive"}/${changeId}`;
|
|
32
|
+
}
|
|
33
|
+
function displayChangePath(config, changeId) {
|
|
34
|
+
return `${changesPathRel(config)}/${changeId}`;
|
|
35
|
+
}
|
|
36
|
+
function guidancePaths(config) {
|
|
37
|
+
const memory = memoryPathRel(config);
|
|
38
|
+
const changes = changesPathRel(config);
|
|
39
|
+
return {
|
|
40
|
+
memory,
|
|
41
|
+
changes,
|
|
42
|
+
constitution: `${memory}/constitution.md`,
|
|
43
|
+
memoryIndex: `${memory}/index.md`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function changesDir(projectRoot, config) {
|
|
47
|
+
return join(projectRoot, config.changes_path);
|
|
48
|
+
}
|
|
49
|
+
function archiveDir(projectRoot, config) {
|
|
50
|
+
return join(projectRoot, config.archive_path);
|
|
51
|
+
}
|
|
52
|
+
function domainsDir(projectRoot, config) {
|
|
53
|
+
return join(projectRoot, config.domains_path);
|
|
54
|
+
}
|
|
55
|
+
function changePath(projectRoot, config, changeId) {
|
|
56
|
+
return join(changesDir(projectRoot, config), changeId);
|
|
57
|
+
}
|
|
58
|
+
function changeMetaPath(projectRoot, config, changeId) {
|
|
59
|
+
return join(changePath(projectRoot, config, changeId), "meta.yaml");
|
|
60
|
+
}
|
|
61
|
+
function activePointerPath(projectRoot, config) {
|
|
62
|
+
return join(changesDir(projectRoot, config), ".active");
|
|
63
|
+
}
|
|
64
|
+
function searchConfigPath(projectRoot) {
|
|
65
|
+
return join(sddRoot(projectRoot), "search.yaml");
|
|
66
|
+
}
|
|
67
|
+
function indexDbPath(projectRoot) {
|
|
68
|
+
return join(sddRoot(projectRoot), "index.db");
|
|
69
|
+
}
|
|
70
|
+
|
|
1
71
|
// src/fs.ts
|
|
2
72
|
import { promises as fs } from "fs";
|
|
3
73
|
import { dirname } from "pathe";
|
|
@@ -81,6 +151,25 @@ async function removePath(path) {
|
|
|
81
151
|
}
|
|
82
152
|
|
|
83
153
|
export {
|
|
154
|
+
SDD_DIR,
|
|
155
|
+
sddRoot,
|
|
156
|
+
configPath,
|
|
157
|
+
workflowsDir,
|
|
158
|
+
templatesDir,
|
|
159
|
+
memoryDir,
|
|
160
|
+
memoryPathRel,
|
|
161
|
+
changesPathRel,
|
|
162
|
+
displayArchivePath,
|
|
163
|
+
displayChangePath,
|
|
164
|
+
guidancePaths,
|
|
165
|
+
changesDir,
|
|
166
|
+
archiveDir,
|
|
167
|
+
domainsDir,
|
|
168
|
+
changePath,
|
|
169
|
+
changeMetaPath,
|
|
170
|
+
activePointerPath,
|
|
171
|
+
searchConfigPath,
|
|
172
|
+
indexDbPath,
|
|
84
173
|
pathExists,
|
|
85
174
|
ensureDir,
|
|
86
175
|
readText,
|