@weave-tools/weave-it 0.1.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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +868 -0
  3. package/dist/cli.d.ts +5 -0
  4. package/dist/cli.js +4538 -0
  5. package/dist/cli.js.map +1 -0
  6. package/package.json +65 -0
  7. package/templates/opencode/commands/weave-architect.md +7 -0
  8. package/templates/opencode/commands/weave-capture.md +7 -0
  9. package/templates/opencode/commands/weave-clarify.md +7 -0
  10. package/templates/opencode/commands/weave-execute.md +7 -0
  11. package/templates/opencode/commands/weave-explore.md +7 -0
  12. package/templates/opencode/commands/weave-issues.md +7 -0
  13. package/templates/opencode/commands/weave-knowledge.md +7 -0
  14. package/templates/opencode/commands/weave-new.md +7 -0
  15. package/templates/opencode/commands/weave-next.md +7 -0
  16. package/templates/opencode/commands/weave-prd.md +7 -0
  17. package/templates/opencode/commands/weave-prepare.md +7 -0
  18. package/templates/skills/weave-architect/SKILL.md +291 -0
  19. package/templates/skills/weave-architect/api-contract-template.md +21 -0
  20. package/templates/skills/weave-architect/frontend-backend-template.md +21 -0
  21. package/templates/skills/weave-architect/index-template.md +24 -0
  22. package/templates/skills/weave-architect/schema-template.md +21 -0
  23. package/templates/skills/weave-capture/SKILL.md +398 -0
  24. package/templates/skills/weave-clarify/SKILL.md +513 -0
  25. package/templates/skills/weave-execute/SKILL.md +215 -0
  26. package/templates/skills/weave-explore/SKILL.md +434 -0
  27. package/templates/skills/weave-issues/SKILL.md +441 -0
  28. package/templates/skills/weave-knowledge/SKILL.md +161 -0
  29. package/templates/skills/weave-knowledge/knowledge-templates.md +63 -0
  30. package/templates/skills/weave-new/SKILL.md +76 -0
  31. package/templates/skills/weave-next/SKILL.md +216 -0
  32. package/templates/skills/weave-prd/SKILL.md +419 -0
  33. package/templates/skills/weave-prd/prd-template.md +186 -0
  34. package/templates/skills/weave-prepare/SKILL.md +94 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4538 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command10 } from "commander";
5
+
6
+ // src/commands/add.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/lib/add-folder.ts
10
+ import path6 from "path";
11
+ import { realpath as realpath4, stat as stat2 } from "fs/promises";
12
+
13
+ // src/lib/folders.ts
14
+ import path from "path";
15
+ import { realpath } from "fs/promises";
16
+
17
+ // src/lib/files.ts
18
+ import { constants } from "fs";
19
+ import { access, mkdir, readFile, readdir, rename, stat, writeFile } from "fs/promises";
20
+ import { dirname } from "path";
21
+ async function pathExists(path14) {
22
+ try {
23
+ await access(path14, constants.F_OK);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function writeNewFile(path14, contents) {
30
+ await writeFile(path14, contents, { flag: "wx" });
31
+ }
32
+ async function writeFileIfMissing(path14, contents) {
33
+ if (await pathExists(path14)) {
34
+ return false;
35
+ }
36
+ await writeNewFile(path14, contents);
37
+ return true;
38
+ }
39
+ async function writeFileAtomic(path14, contents) {
40
+ const tempPath = `${path14}.${process.pid}.tmp`;
41
+ await writeFile(tempPath, contents);
42
+ await rename(tempPath, path14);
43
+ }
44
+ async function ensureDir(path14) {
45
+ await mkdir(path14, { recursive: true });
46
+ }
47
+ async function createDirExclusive(path14) {
48
+ await mkdir(path14, { recursive: false });
49
+ }
50
+ async function ensureDirectory(path14) {
51
+ const value = await stat(path14);
52
+ if (!value.isDirectory()) {
53
+ throw new Error(`Expected a directory: ${path14}`);
54
+ }
55
+ }
56
+ async function isDirectoryEmpty(path14) {
57
+ const entries = await readdir(path14);
58
+ return entries.length === 0;
59
+ }
60
+ async function movePath(source, target) {
61
+ await rename(source, target);
62
+ }
63
+ async function readJsonCache(path14) {
64
+ try {
65
+ const contents = await readFile(path14, "utf8");
66
+ return JSON.parse(contents);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ async function writeJsonCache(path14, data) {
72
+ try {
73
+ await mkdir(dirname(path14), { recursive: true });
74
+ await writeFileAtomic(path14, JSON.stringify(data, null, 2));
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ // src/lib/git.ts
82
+ import { execFile } from "child_process";
83
+ import { promisify } from "util";
84
+ var execFileAsync = promisify(execFile);
85
+ async function git(args, cwd) {
86
+ try {
87
+ const { stdout } = await execFileAsync("git", args, { cwd, env: gitEnv() });
88
+ const value = stdout.trim();
89
+ return value.length > 0 ? value : void 0;
90
+ } catch {
91
+ return void 0;
92
+ }
93
+ }
94
+ function gitEnv() {
95
+ return {
96
+ ...process.env,
97
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "Weave",
98
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "weave@example.invalid",
99
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "Weave",
100
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "weave@example.invalid"
101
+ };
102
+ }
103
+ async function findGitRoot(cwd) {
104
+ return git(["rev-parse", "--show-toplevel"], cwd);
105
+ }
106
+ async function currentBranch(cwd) {
107
+ const gitRoot = await findGitRoot(cwd);
108
+ if (!gitRoot) {
109
+ return void 0;
110
+ }
111
+ return git(["branch", "--show-current"], gitRoot);
112
+ }
113
+ async function isWorktreeDirty(cwd) {
114
+ const gitRoot = await findGitRoot(cwd);
115
+ if (!gitRoot) {
116
+ return false;
117
+ }
118
+ return Boolean(await git(["status", "--porcelain"], gitRoot));
119
+ }
120
+ async function branchExists(cwd, branch) {
121
+ const gitRoot = await findGitRoot(cwd);
122
+ if (!gitRoot) {
123
+ return false;
124
+ }
125
+ return Boolean(await git(["rev-parse", "--verify", `refs/heads/${branch}`], gitRoot));
126
+ }
127
+ async function checkoutBranch(cwd, branch) {
128
+ const gitRoot = await findGitRoot(cwd);
129
+ await runGitRequired(["checkout", branch], gitRoot ?? cwd);
130
+ }
131
+ async function createBranch(cwd, branch) {
132
+ const gitRoot = await findGitRoot(cwd);
133
+ await runGitRequired(["checkout", "-b", branch], gitRoot ?? cwd);
134
+ }
135
+ async function currentHead(cwd) {
136
+ const gitRoot = await findGitRoot(cwd);
137
+ if (!gitRoot) {
138
+ return void 0;
139
+ }
140
+ return git(["rev-parse", "HEAD"], gitRoot);
141
+ }
142
+ async function getGitRemote(cwd) {
143
+ return git(["config", "--get", "remote.origin.url"], cwd);
144
+ }
145
+ async function runGitRequired(args, cwd) {
146
+ await execFileAsync("git", args, { cwd, env: gitEnv() });
147
+ }
148
+ async function cloneRepo(url, destinationDir, cwd) {
149
+ await runGitRequired(["clone", "--", url, destinationDir], cwd);
150
+ }
151
+
152
+ // src/lib/ids.ts
153
+ function slugify(value, fallback) {
154
+ const slug = value.trim().toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
155
+ return slug.length > 0 ? slug : fallback;
156
+ }
157
+ function titleFromSlug(value) {
158
+ return value.replace(/[-_]+/g, " ").split(" ").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
159
+ }
160
+
161
+ // src/lib/folders.ts
162
+ async function resolveFolder(input) {
163
+ const candidate = input.targetPath ? path.resolve(input.cwd, input.targetPath) : input.cwd;
164
+ await ensureDirectory(candidate);
165
+ const gitRoot = await findGitRoot(candidate);
166
+ const folderPath = await realpath(gitRoot ?? candidate);
167
+ const basename2 = path.basename(folderPath);
168
+ const id = input.id ?? slugify(basename2, "folder");
169
+ const gitRemote = gitRoot ? await getGitRemote(folderPath) : void 0;
170
+ return {
171
+ id,
172
+ path: folderPath,
173
+ name: titleFromSlug(id) || basename2,
174
+ kind: input.kind ?? "app",
175
+ gitRemote
176
+ };
177
+ }
178
+
179
+ // src/lib/session-state.ts
180
+ import os from "os";
181
+ import path2 from "path";
182
+ import { readFile as readFile2 } from "fs/promises";
183
+ import YAML from "yaml";
184
+ function defaultSessionPath() {
185
+ if (process.env.WEAVE_SESSION_PATH) {
186
+ return path2.resolve(process.env.WEAVE_SESSION_PATH);
187
+ }
188
+ return path2.join(os.homedir(), ".cache", "weave", "current-session.yml");
189
+ }
190
+ async function loadCurrentSession(sessionPath = defaultSessionPath()) {
191
+ if (!await pathExists(sessionPath)) {
192
+ return void 0;
193
+ }
194
+ return YAML.parse(await readFile2(sessionPath, "utf8"));
195
+ }
196
+ async function saveCurrentSession(session, sessionPath = defaultSessionPath()) {
197
+ await ensureDir(path2.dirname(sessionPath));
198
+ await writeFileAtomic(sessionPath, YAML.stringify(session));
199
+ }
200
+ function createCurrentSession(folder, now) {
201
+ const session = {
202
+ version: 1,
203
+ updated_at: now.toISOString(),
204
+ folders: {}
205
+ };
206
+ addFolderToSession(session, folder, now);
207
+ return session;
208
+ }
209
+ function addFolderToSession(session, folder, now) {
210
+ const existingId = findFolderByPath(session, folder.path);
211
+ if (existingId) {
212
+ session.updated_at = now.toISOString();
213
+ return { added: false, id: existingId, session };
214
+ }
215
+ const id = uniqueFolderId(session, folder.id);
216
+ const sessionFolder = {
217
+ path: folder.path,
218
+ name: folder.name,
219
+ kind: folder.kind
220
+ };
221
+ if (folder.gitRemote) {
222
+ sessionFolder.git_remote = folder.gitRemote;
223
+ }
224
+ session.folders[id] = sessionFolder;
225
+ session.updated_at = now.toISOString();
226
+ return { added: true, id, session };
227
+ }
228
+ function findFolderByPath(session, folderPath) {
229
+ return Object.entries(session.folders).find(([, folder]) => folder.path === folderPath)?.[0];
230
+ }
231
+ function ensureFolderInSession(session, folderPath, now) {
232
+ const existingId = findFolderByPath(session, folderPath);
233
+ if (existingId) {
234
+ session.updated_at = now.toISOString();
235
+ return existingId;
236
+ }
237
+ const basename2 = path2.basename(folderPath);
238
+ const id = uniqueFolderId(session, slugify(basename2, "folder"));
239
+ session.folders[id] = {
240
+ path: folderPath,
241
+ name: titleFromSlug(id) || basename2,
242
+ kind: "app"
243
+ };
244
+ session.updated_at = now.toISOString();
245
+ return id;
246
+ }
247
+ function setCurrentChangeForPath(session, folderPath, change, now) {
248
+ const id = ensureFolderInSession(session, folderPath, now);
249
+ session.folders[id].current_change = {
250
+ ...change,
251
+ updated_at: now.toISOString()
252
+ };
253
+ session.updated_at = now.toISOString();
254
+ return id;
255
+ }
256
+ function setCurrentArtifactForPath(session, folderPath, artifact, now) {
257
+ const id = ensureFolderInSession(session, folderPath, now);
258
+ session.folders[id].current_artifact = {
259
+ ...artifact,
260
+ updated_at: now.toISOString()
261
+ };
262
+ session.updated_at = now.toISOString();
263
+ return id;
264
+ }
265
+ function clearCurrentArtifactForPath(session, folderPath, now) {
266
+ const id = ensureFolderInSession(session, folderPath, now);
267
+ delete session.folders[id].current_artifact;
268
+ session.updated_at = now.toISOString();
269
+ return id;
270
+ }
271
+ function currentArtifactForPath(session, folderPath) {
272
+ const id = session ? findFolderByPath(session, folderPath) : void 0;
273
+ return id ? session?.folders[id]?.current_artifact : void 0;
274
+ }
275
+ function uniqueFolderId(session, id) {
276
+ if (!session.folders[id]) {
277
+ return id;
278
+ }
279
+ let index = 2;
280
+ while (session.folders[`${id}-${index}`]) {
281
+ index += 1;
282
+ }
283
+ return `${id}-${index}`;
284
+ }
285
+
286
+ // src/lib/weave-scaffold.ts
287
+ import path3 from "path";
288
+ import YAML2 from "yaml";
289
+
290
+ // src/lib/sync.ts
291
+ import { createHash } from "crypto";
292
+ function hashDocument(contents) {
293
+ return `sha256:${createHash("sha256").update(contents).digest("hex")}`;
294
+ }
295
+
296
+ // src/lib/weave-scaffold.ts
297
+ async function ensureWeaveScaffold(input) {
298
+ const wikiDir = path3.join(input.folder.path, "wiki");
299
+ const metadataDir = path3.join(input.folder.path, ".weave");
300
+ const created = [];
301
+ const knowledgeIndex = knowledgeIndexTemplate();
302
+ const knowledgeReadme = knowledgeReadmeTemplate();
303
+ const domainsReadme = domainsReadmeTemplate();
304
+ const sharedReadme = sharedReadmeTemplate();
305
+ const knowledgeDir = path3.join(wikiDir, "knowledge");
306
+ const changesDir = path3.join(wikiDir, "changes");
307
+ const changesExisted = await pathExists(changesDir);
308
+ await ensureDir(knowledgeDir);
309
+ await ensureDir(path3.join(knowledgeDir, "domains"));
310
+ await ensureDir(path3.join(knowledgeDir, "shared"));
311
+ await ensureDir(changesDir);
312
+ await ensureDir(metadataDir);
313
+ if (await writeFileIfMissing(path3.join(metadataDir, "sync.yml"), syncTemplate(knowledgeIndex))) {
314
+ created.push(".weave/sync.yml");
315
+ }
316
+ if (await writeFileIfMissing(path3.join(knowledgeDir, "index.md"), knowledgeIndex)) {
317
+ created.push("wiki/knowledge/index.md");
318
+ }
319
+ if (await writeFileIfMissing(path3.join(knowledgeDir, "README.md"), knowledgeReadme)) {
320
+ created.push("wiki/knowledge/README.md");
321
+ }
322
+ if (await writeFileIfMissing(path3.join(knowledgeDir, "domains", "README.md"), domainsReadme)) {
323
+ created.push("wiki/knowledge/domains/README.md");
324
+ }
325
+ if (await writeFileIfMissing(path3.join(knowledgeDir, "shared", "README.md"), sharedReadme)) {
326
+ created.push("wiki/knowledge/shared/README.md");
327
+ }
328
+ if (!changesExisted) {
329
+ created.push("wiki/changes/");
330
+ }
331
+ return { wikiDir, metadataDir, created };
332
+ }
333
+ function syncTemplate(knowledgeIndex) {
334
+ return YAML2.stringify({
335
+ version: 1,
336
+ documents: {
337
+ "knowledge.index": {
338
+ path: "wiki/knowledge/index.md",
339
+ hash: hashDocument(knowledgeIndex),
340
+ status: "synced"
341
+ }
342
+ }
343
+ });
344
+ }
345
+ function knowledgeIndexTemplate() {
346
+ return `# Product Knowledge
347
+
348
+ This folder contains current product knowledge for this repo/folder.
349
+
350
+ ## Domains
351
+
352
+ _Add product domains here as they become relevant._
353
+
354
+ Examples:
355
+ - Billing
356
+ - Permissions
357
+ - Onboarding
358
+ - Notifications
359
+ `;
360
+ }
361
+ function knowledgeReadmeTemplate() {
362
+ return `# Knowledge Structure
363
+
364
+ This folder contains current-state behavioral specs. Historical change provenance belongs in \`wiki/changes/**\`.
365
+
366
+ Use this progressive structure:
367
+
368
+ \`\`\`text
369
+ wiki/knowledge/
370
+ index.md
371
+ README.md
372
+ domains/
373
+ README.md
374
+ <domain>/
375
+ index.md
376
+ overview.md
377
+ glossary.md
378
+ source-map.md
379
+ features/
380
+ <feature>/
381
+ behavior.md
382
+ decision-tables.md
383
+ lifecycle.md
384
+ permissions.md
385
+ source-map.md
386
+ domain-wide/
387
+ lifecycle.md
388
+ permissions.md
389
+ visibility.md
390
+ notifications.md
391
+ approvals.md
392
+ edge-cases.md
393
+ shared/
394
+ README.md
395
+ <shared-behavior>/
396
+ behavior.md
397
+ source-map.md
398
+ \`\`\`
399
+
400
+ - \`domains/\` contains product or system areas users naturally name.
401
+ - \`features/\` contains independently understandable behavior inside a domain.
402
+ - \`domain-wide/\` contains behavior that coordinates multiple features inside one domain.
403
+ - \`shared/\` contains behavior reused across multiple domains.
404
+
405
+ ## Guided Templates
406
+
407
+ \`behavior.md\` is the core current-state spec:
408
+
409
+ \`\`\`md
410
+ # <Feature Or Shared Behavior>
411
+
412
+ ## Purpose
413
+ ## Current Behavior
414
+ ## Domain Model
415
+ ## Configuration Dimensions
416
+ ## Behavioral Rules
417
+ ## Decision Tables
418
+ ## Lifecycle
419
+ ## Permissions And Visibility
420
+ ## Integrations And Side Effects
421
+ ## Edge Cases
422
+ ## Invariants
423
+ ## Source Anchors
424
+ ## Change History
425
+ ## Open Questions
426
+ \`\`\`
427
+
428
+ \`Purpose\`, \`Current Behavior\`, \`Source Anchors\`, and \`Change History\` are strongly recommended. Other sections may be omitted when they do not apply.
429
+
430
+ \`decision-tables.md\` is optional and focused on permutations:
431
+
432
+ \`\`\`md
433
+ # <Feature> Decision Tables
434
+
435
+ ## Table: <Scenario>
436
+
437
+ | Dimension | Value | Outcome |
438
+ | --- | --- | --- |
439
+
440
+ ## Notes
441
+ ## Source Anchors
442
+ \`\`\`
443
+
444
+ \`source-map.md\` connects behavior to reality:
445
+
446
+ \`\`\`md
447
+ # <Domain Or Feature> Source Map
448
+
449
+ ## Core Product Surfaces
450
+ ## Source Anchors
451
+ ## Tests
452
+ ## Config And Flags
453
+ ## Jobs And Side Effects
454
+ ## External Integrations
455
+ ## Ownership Notes
456
+ \`\`\`
457
+
458
+ \`knowledge-delta.md\` lives under \`wiki/changes/<change-id>/\` and bridges one change to current knowledge:
459
+
460
+ \`\`\`md
461
+ # Knowledge Delta
462
+
463
+ ## Durable Behavior Changes
464
+ ## Affected Knowledge Areas
465
+ ## Knowledge Files Updated
466
+ ## No-Impact Rationale
467
+ ## Source Evidence
468
+ ## Follow-Up Knowledge Work
469
+ \`\`\`
470
+ `;
471
+ }
472
+ function domainsReadmeTemplate() {
473
+ return `# Domains
474
+
475
+ Create one folder per product or system domain. A domain folder may start small and grow as behavior becomes heavier.
476
+
477
+ Recommended domain files:
478
+
479
+ - \`index.md\`: entry point and links to important specs
480
+ - \`overview.md\`: domain summary
481
+ - \`glossary.md\`: domain terms
482
+ - \`source-map.md\`: code, tests, flags, jobs, integrations, and ownership
483
+ - \`features/<feature>/behavior.md\`: feature-level current behavior
484
+ - \`domain-wide/*.md\`: behavior that coordinates multiple features in the domain
485
+ `;
486
+ }
487
+ function sharedReadmeTemplate() {
488
+ return `# Shared Behavior
489
+
490
+ Use this folder for behavior reused across multiple domains, such as permissions, approvals, notifications, audit logs, privacy, retention, imports, exports, and integrations.
491
+
492
+ Prefer keeping behavior inside one domain until multiple domains depend on it. When behavior becomes shared, document the shared model here and the domain-specific integration inside the domain.
493
+ `;
494
+ }
495
+
496
+ // src/lib/workspace-mode.ts
497
+ import { readFile as readFile3 } from "fs/promises";
498
+ import path4 from "path";
499
+ import { realpath as realpath2 } from "fs/promises";
500
+ import YAML3 from "yaml";
501
+ async function findWorkspaceMode(startPath) {
502
+ let current = await realpath2(startPath);
503
+ for (; ; ) {
504
+ const workspaceYmlPath = path4.join(current, ".weave", "workspace.yml");
505
+ if (await pathExists(workspaceYmlPath)) {
506
+ const parsed = await readWorkspaceYml(workspaceYmlPath);
507
+ if (!parsed) {
508
+ return void 0;
509
+ }
510
+ const mode = parsed.mode === "workspace" ? "workspace" : "repo";
511
+ return {
512
+ mode,
513
+ workspacePath: current,
514
+ workspaceYmlPath
515
+ };
516
+ }
517
+ const parent = path4.dirname(current);
518
+ if (parent === current) {
519
+ return void 0;
520
+ }
521
+ current = parent;
522
+ }
523
+ }
524
+ async function resolveChangeContext(startPath, sessionPath = defaultSessionPath()) {
525
+ const workspace = await findWorkspaceMode(startPath);
526
+ if (!workspace) {
527
+ return void 0;
528
+ }
529
+ const session = await loadCurrentSession(sessionPath);
530
+ const folderId = session ? findFolderByPath(session, workspace.workspacePath) : void 0;
531
+ const folder = folderId ? session?.folders[folderId] : void 0;
532
+ return {
533
+ mode: workspace.mode,
534
+ rootPath: workspace.workspacePath,
535
+ workspaceYmlPath: workspace.workspaceYmlPath,
536
+ folderId,
537
+ folderName: folder?.name
538
+ };
539
+ }
540
+ async function readWorkspaceYml(workspaceYmlPath) {
541
+ try {
542
+ const parsed = YAML3.parse(await readFile3(workspaceYmlPath, "utf8"));
543
+ if (!parsed || typeof parsed !== "object") {
544
+ return void 0;
545
+ }
546
+ return parsed;
547
+ } catch {
548
+ return void 0;
549
+ }
550
+ }
551
+ function isWorkspaceMode(result3) {
552
+ return result3?.mode === "workspace";
553
+ }
554
+
555
+ // src/lib/workspace-repos.ts
556
+ import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
557
+ import path5 from "path";
558
+ import { realpath as realpath3 } from "fs/promises";
559
+ import YAML4 from "yaml";
560
+ var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/)/i;
561
+ function isGitUrl(input) {
562
+ return GIT_URL_PATTERN.test(input.trim());
563
+ }
564
+ function repoNameFromUrl(url) {
565
+ const trimmed = url.trim().replace(/\/$/, "");
566
+ const lastSegment = trimmed.split("/").pop() ?? trimmed;
567
+ const withoutGit = lastSegment.replace(/\.git$/i, "");
568
+ return withoutGit || "repo";
569
+ }
570
+ async function isInsideWorkspace(workspacePath, candidatePath) {
571
+ const resolvedWorkspace = await realpath3(workspacePath);
572
+ const resolvedCandidate = await realpath3(candidatePath);
573
+ const relative2 = path5.relative(resolvedWorkspace, resolvedCandidate);
574
+ if (relative2 === "") {
575
+ return true;
576
+ }
577
+ if (path5.isAbsolute(relative2)) {
578
+ return false;
579
+ }
580
+ return !relative2.startsWith(`..${path5.sep}`) && relative2 !== "..";
581
+ }
582
+ function relativeRepoPath(workspacePath, repoAbsolutePath) {
583
+ const relative2 = path5.relative(workspacePath, repoAbsolutePath);
584
+ return relative2.split(path5.sep).join("/");
585
+ }
586
+ function gitignoreEntryForRelativePath(relativePath) {
587
+ const normalized = relativePath.split(path5.sep).join("/").replace(/^\/+/, "").replace(/\/+$/, "");
588
+ return `/${normalized}/`;
589
+ }
590
+ async function readWorkspaceMetadata(workspacePath) {
591
+ const workspaceYmlPath = path5.join(workspacePath, ".weave", "workspace.yml");
592
+ if (!await pathExists(workspaceYmlPath)) {
593
+ return void 0;
594
+ }
595
+ try {
596
+ const parsed = YAML4.parse(await readFile4(workspaceYmlPath, "utf8"));
597
+ if (!parsed || typeof parsed !== "object") {
598
+ return void 0;
599
+ }
600
+ const repos = parseRepos(parsed.repos);
601
+ return {
602
+ version: typeof parsed.version === "number" ? parsed.version : void 0,
603
+ mode: typeof parsed.mode === "string" ? parsed.mode : void 0,
604
+ name: typeof parsed.name === "string" ? parsed.name : void 0,
605
+ repos
606
+ };
607
+ } catch {
608
+ return void 0;
609
+ }
610
+ }
611
+ function parseRepos(value) {
612
+ if (!value || typeof value !== "object") {
613
+ return {};
614
+ }
615
+ const repos = {};
616
+ for (const [id, entry] of Object.entries(value)) {
617
+ if (!entry || typeof entry !== "object") {
618
+ continue;
619
+ }
620
+ const record = entry;
621
+ const repoPath = typeof record.path === "string" ? record.path : void 0;
622
+ const kind = typeof record.kind === "string" ? record.kind : void 0;
623
+ if (!repoPath || !kind) {
624
+ continue;
625
+ }
626
+ repos[id] = {
627
+ path: repoPath,
628
+ kind,
629
+ ...typeof record.remote === "string" ? { remote: record.remote } : {}
630
+ };
631
+ }
632
+ return repos;
633
+ }
634
+ function findRegisteredRepoByPath(metadata, relativePath) {
635
+ const normalized = normalizeRepoPath(relativePath);
636
+ return Object.entries(metadata.repos).find(([, entry]) => normalizeRepoPath(entry.path) === normalized)?.[0];
637
+ }
638
+ function normalizeRepoPath(repoPath) {
639
+ return repoPath.split(path5.sep).join("/").replace(/^\/+/, "").replace(/\/+$/, "");
640
+ }
641
+ async function appendGitignoreEntry(workspacePath, relativePath) {
642
+ const gitignorePath = path5.join(workspacePath, ".gitignore");
643
+ const entry = gitignoreEntryForRelativePath(relativePath);
644
+ const existing = await pathExists(gitignorePath) ? await readFile4(gitignorePath, "utf8") : "";
645
+ const lines = existing.split("\n");
646
+ if (lines.some((line) => line.trim() === entry.trim())) {
647
+ return;
648
+ }
649
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
650
+ const prefix = existing.length > 0 ? separator : "";
651
+ await writeFile2(gitignorePath, `${existing}${prefix}${entry}
652
+ `);
653
+ }
654
+ async function registerRepoInWorkspaceMetadata(workspacePath, input) {
655
+ const workspaceYmlPath = path5.join(workspacePath, ".weave", "workspace.yml");
656
+ const metadata = await readWorkspaceMetadata(workspacePath) ?? {
657
+ version: 1,
658
+ mode: "workspace",
659
+ name: path5.basename(workspacePath),
660
+ repos: {}
661
+ };
662
+ const normalizedPath = normalizeRepoPath(input.relativePath);
663
+ const existing = metadata.repos[input.id];
664
+ if (existing && normalizeRepoPath(existing.path) !== normalizedPath) {
665
+ throw new Error(
666
+ `Workspace repo id "${input.id}" is already registered for a different path (${existing.path}). Pass --id with a different value.`
667
+ );
668
+ }
669
+ const collision = Object.entries(metadata.repos).find(
670
+ ([id, entry]) => id !== input.id && normalizeRepoPath(entry.path) === normalizedPath
671
+ );
672
+ if (collision) {
673
+ throw new Error(`Path is already registered in workspace as "${collision[0]}".`);
674
+ }
675
+ metadata.repos[input.id] = {
676
+ path: normalizedPath,
677
+ kind: input.kind,
678
+ ...input.remote ? { remote: input.remote } : {}
679
+ };
680
+ await writeFile2(
681
+ workspaceYmlPath,
682
+ YAML4.stringify({
683
+ version: metadata.version ?? 1,
684
+ mode: metadata.mode ?? "workspace",
685
+ name: metadata.name ?? path5.basename(workspacePath),
686
+ repos: metadata.repos
687
+ })
688
+ );
689
+ }
690
+ async function registerRepoIntoWorkspace(input) {
691
+ await appendGitignoreEntry(input.workspacePath, input.relativePath);
692
+ await registerRepoInWorkspaceMetadata(input.workspacePath, {
693
+ id: input.id,
694
+ relativePath: input.relativePath,
695
+ kind: input.kind,
696
+ remote: input.remote
697
+ });
698
+ return {
699
+ id: input.id,
700
+ relativePath: input.relativePath
701
+ };
702
+ }
703
+ function defaultRepoId(relativePath, explicitId) {
704
+ if (explicitId) {
705
+ return explicitId;
706
+ }
707
+ const basename2 = path5.basename(relativePath.split("/").join(path5.sep));
708
+ return slugify(basename2, "repo");
709
+ }
710
+ function workspaceGitignoreBaseTemplate() {
711
+ return [".DS_Store", "node_modules/"].join("\n") + "\n";
712
+ }
713
+ function listReposForDisplay(metadata) {
714
+ return Object.entries(metadata.repos).map(([id, entry]) => ({
715
+ id,
716
+ path: entry.path,
717
+ kind: entry.kind,
718
+ ...entry.remote ? { remote: entry.remote } : {}
719
+ }));
720
+ }
721
+
722
+ // src/lib/add-folder.ts
723
+ async function addFolder(options) {
724
+ const cwd = options.cwd ?? process.cwd();
725
+ const now = options.now ?? /* @__PURE__ */ new Date();
726
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
727
+ const session = await loadCurrentSession(sessionPath);
728
+ if (!session) {
729
+ return {
730
+ status: "no_session",
731
+ message: "No current Weave session found. Run `weave init` first."
732
+ };
733
+ }
734
+ const modeResult = await findWorkspaceMode(cwd);
735
+ if (isWorkspaceMode(modeResult)) {
736
+ return addFolderToWorkspace({
737
+ cwd,
738
+ workspacePath: modeResult.workspacePath,
739
+ targetPath: options.targetPath,
740
+ folderId: options.folderId,
741
+ folderKind: options.folderKind ?? "app"
742
+ });
743
+ }
744
+ return addFolderToRepoSession({
745
+ cwd,
746
+ targetPath: options.targetPath,
747
+ folderId: options.folderId,
748
+ folderKind: options.folderKind,
749
+ session,
750
+ sessionPath,
751
+ now
752
+ });
753
+ }
754
+ async function addFolderToRepoSession(input) {
755
+ const folder = await resolveFolder({
756
+ cwd: input.cwd,
757
+ targetPath: input.targetPath,
758
+ id: input.folderId,
759
+ kind: input.folderKind
760
+ });
761
+ await ensureWeaveScaffold({ folder });
762
+ const result3 = addFolderToSession(input.session, folder, input.now);
763
+ await saveCurrentSession(result3.session, input.sessionPath);
764
+ if (!result3.added) {
765
+ return {
766
+ status: "already_exists",
767
+ message: `Folder already exists in current Weave session: ${result3.id}
768
+
769
+ Path:
770
+ ${folder.path}`
771
+ };
772
+ }
773
+ return {
774
+ status: "added",
775
+ message: `Added folder to current Weave session: ${result3.id}
776
+
777
+ Path:
778
+ ${folder.path}
779
+
780
+ Next:
781
+ weave workspace`
782
+ };
783
+ }
784
+ async function addFolderToWorkspace(input) {
785
+ const workspacePath = await realpath4(input.workspacePath);
786
+ const metadata = await readWorkspaceMetadata(workspacePath);
787
+ if (!metadata) {
788
+ throw new Error(`Workspace metadata is missing or invalid: ${path6.join(workspacePath, ".weave", "workspace.yml")}`);
789
+ }
790
+ let repoAbsolutePath;
791
+ let relativePath;
792
+ let remote;
793
+ if (isGitUrl(input.targetPath)) {
794
+ const url = input.targetPath.trim();
795
+ const destName = repoNameFromUrl(url);
796
+ relativePath = destName;
797
+ repoAbsolutePath = path6.join(workspacePath, destName);
798
+ if (await pathExists(repoAbsolutePath)) {
799
+ throw new Error(`Destination already exists: ${repoAbsolutePath}`);
800
+ }
801
+ const existingId = findRegisteredRepoByPath(metadata, relativePath);
802
+ if (existingId) {
803
+ return workspaceAlreadyRegisteredMessage(existingId, relativePath, workspacePath);
804
+ }
805
+ try {
806
+ await cloneRepo(url, destName, workspacePath);
807
+ } catch (error) {
808
+ const detail = error instanceof Error ? error.message : String(error);
809
+ throw new Error(`Failed to clone repository: ${detail}`);
810
+ }
811
+ remote = url;
812
+ } else {
813
+ const candidate = path6.resolve(input.cwd, input.targetPath);
814
+ await assertDirectory(candidate);
815
+ if (await isInsideWorkspace(workspacePath, candidate)) {
816
+ repoAbsolutePath = await realpath4(candidate);
817
+ relativePath = relativeRepoPath(workspacePath, repoAbsolutePath);
818
+ } else {
819
+ const sourcePath = await realpath4(candidate);
820
+ const destName = path6.basename(sourcePath);
821
+ relativePath = destName;
822
+ repoAbsolutePath = path6.join(workspacePath, destName);
823
+ if (await pathExists(repoAbsolutePath)) {
824
+ throw new Error(`Destination already exists in workspace: ${repoAbsolutePath}`);
825
+ }
826
+ try {
827
+ await movePath(sourcePath, repoAbsolutePath);
828
+ } catch (error) {
829
+ const detail = error instanceof Error ? error.message : String(error);
830
+ throw new Error(`Failed to adopt folder into workspace: ${detail}`);
831
+ }
832
+ }
833
+ const existingId = findRegisteredRepoByPath(metadata, relativePath);
834
+ if (existingId) {
835
+ return workspaceAlreadyRegisteredMessage(existingId, relativePath, workspacePath);
836
+ }
837
+ remote = await getGitRemote(repoAbsolutePath);
838
+ }
839
+ const id = defaultRepoId(relativePath, input.folderId);
840
+ await registerRepoIntoWorkspace({
841
+ workspacePath,
842
+ id,
843
+ relativePath,
844
+ kind: input.folderKind,
845
+ remote
846
+ });
847
+ const remoteText = remote ? `
848
+
849
+ Remote:
850
+ ${remote}` : "";
851
+ return {
852
+ status: "added",
853
+ message: `Registered repo in workspace: ${id}
854
+
855
+ Path:
856
+ ${relativePath}${remoteText}
857
+
858
+ Workspace:
859
+ ${workspacePath}
860
+
861
+ Next:
862
+ weave workspace`
863
+ };
864
+ }
865
+ function workspaceAlreadyRegisteredMessage(id, relativePath, workspacePath) {
866
+ return {
867
+ status: "already_exists",
868
+ message: `Repo already registered in workspace: ${id}
869
+
870
+ Path:
871
+ ${relativePath}
872
+
873
+ Workspace:
874
+ ${workspacePath}`
875
+ };
876
+ }
877
+ async function assertDirectory(targetPath) {
878
+ try {
879
+ const value = await stat2(targetPath);
880
+ if (!value.isDirectory()) {
881
+ throw new Error(`Expected a directory: ${targetPath}`);
882
+ }
883
+ } catch (error) {
884
+ if (error instanceof Error && error.message.startsWith("Expected a directory")) {
885
+ throw error;
886
+ }
887
+ throw new Error(`Expected a directory: ${targetPath}`);
888
+ }
889
+ }
890
+
891
+ // src/commands/add.ts
892
+ function addCommand() {
893
+ return new Command("add").description("Add a folder or git URL to the current Weave context (repo session or workspace registry).").argument("<path>", "folder path or git URL to add").option("--id <id>", "folder id").option("--kind <kind>", "folder kind", "app").action(async (targetPath, options) => {
894
+ const result3 = await addFolder({
895
+ cwd: process.cwd(),
896
+ targetPath,
897
+ folderId: options.id,
898
+ folderKind: options.kind
899
+ });
900
+ process.stdout.write(`${result3.message}
901
+ `);
902
+ if (result3.status === "no_session") {
903
+ process.exitCode = 1;
904
+ }
905
+ });
906
+ }
907
+
908
+ // src/commands/agent.ts
909
+ import { Command as Command2, InvalidArgumentError } from "commander";
910
+
911
+ // src/lib/agent-skills.ts
912
+ import { createHash as createHash2 } from "crypto";
913
+ import { constants as constants2 } from "fs";
914
+ import { access as access2, mkdir as mkdir2, readdir as readdir2, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
915
+ import { basename, dirname as dirname2, extname, join, relative } from "path";
916
+ import { fileURLToPath } from "url";
917
+ import YAML5 from "yaml";
918
+ var manifestRelativePath = join(".weave", "agents.yml");
919
+ async function listDefaultSkills(options = {}) {
920
+ const templatesDir = options.templatesDir ?? await findDefaultSkillsDir();
921
+ const entries = await readdir2(templatesDir, { withFileTypes: true });
922
+ const skills = await Promise.all(
923
+ entries.filter((entry) => entry.isDirectory()).map((entry) => readDefaultSkill(entry.name, { templatesDir }))
924
+ );
925
+ return skills.sort((left, right) => left.name.localeCompare(right.name));
926
+ }
927
+ async function readDefaultSkill(name, options = {}) {
928
+ validateSkillName(name);
929
+ const templatesDir = options.templatesDir ?? await findDefaultSkillsDir();
930
+ const sourcePath = join(templatesDir, name, "SKILL.md");
931
+ const content = await readFile5(sourcePath, "utf8").catch((error) => {
932
+ if (isNodeError(error) && error.code === "ENOENT") {
933
+ throw new Error(`Unknown skill: ${name}`);
934
+ }
935
+ throw error;
936
+ });
937
+ const metadata = parseSkillFrontmatter(content, sourcePath);
938
+ if (metadata.name !== name) {
939
+ throw new Error(`Skill frontmatter name mismatch in ${sourcePath}: expected ${name}, got ${metadata.name}`);
940
+ }
941
+ return {
942
+ name: metadata.name,
943
+ description: metadata.description,
944
+ lastChangedIn: metadata.lastChangedIn,
945
+ sourcePath,
946
+ content,
947
+ hash: hashContent(content)
948
+ };
949
+ }
950
+ async function installAgentSkills(options) {
951
+ const manifest = await loadAgentsManifest(options.cwd);
952
+ const results = [];
953
+ for (const target of resolveAgentTargets(options.cwd, options.agent)) {
954
+ const artifacts = await defaultArtifactsForTarget(target, options);
955
+ for (const artifact of artifacts) {
956
+ results.push(await installArtifact(options.cwd, target.agent, artifact, manifest, options.now ?? /* @__PURE__ */ new Date()));
957
+ }
958
+ }
959
+ await saveAgentsManifest(options.cwd, manifest);
960
+ return summarize(results);
961
+ }
962
+ async function updateAgentSkills(options) {
963
+ const manifest = await loadAgentsManifest(options.cwd);
964
+ const results = [];
965
+ for (const target of resolveAgentTargets(options.cwd, options.agent)) {
966
+ const artifacts = await defaultArtifactsForTarget(target, options);
967
+ for (const artifact of artifacts) {
968
+ results.push(await updateArtifact(options.cwd, target.agent, artifact, manifest, options.now ?? /* @__PURE__ */ new Date()));
969
+ }
970
+ }
971
+ await saveAgentsManifest(options.cwd, manifest);
972
+ return summarize(results);
973
+ }
974
+ async function resetAgentSkills(options) {
975
+ const manifest = await loadAgentsManifest(options.cwd);
976
+ const results = [];
977
+ for (const target of resolveAgentTargets(options.cwd, options.agent)) {
978
+ const artifacts = await defaultArtifactsForTarget(target, options, options.skill);
979
+ for (const artifact of artifacts) {
980
+ results.push(await resetArtifact(options.cwd, target.agent, artifact, manifest, options.now ?? /* @__PURE__ */ new Date()));
981
+ }
982
+ }
983
+ await saveAgentsManifest(options.cwd, manifest);
984
+ return summarize(results);
985
+ }
986
+ async function diffAgentSkills(options) {
987
+ const chunks = [];
988
+ for (const target of resolveAgentTargets(options.cwd, options.agent)) {
989
+ const artifacts = await defaultArtifactsForTarget(target, options, options.skill);
990
+ for (const artifact of artifacts) {
991
+ if (!await pathExists(artifact.destination)) {
992
+ chunks.push(`Missing ${target.agent}/${artifact.kind}/${artifact.name} at ${relative(options.cwd, artifact.destination)}`);
993
+ continue;
994
+ }
995
+ const installed = await readFile5(artifact.destination, "utf8");
996
+ chunks.push(
997
+ formatFullFileDiff(
998
+ relative(options.cwd, artifact.destination),
999
+ `${artifact.kind}:${artifact.name}`,
1000
+ installed,
1001
+ artifact.content
1002
+ )
1003
+ );
1004
+ }
1005
+ }
1006
+ return {
1007
+ status: "ok",
1008
+ message: chunks.join("\n")
1009
+ };
1010
+ }
1011
+ async function defaultArtifactsForTarget(target, options, onlyName) {
1012
+ const skills = onlyName ? [await readDefaultSkill(onlyName, { templatesDir: options.templatesDir })] : await listDefaultSkills({ templatesDir: options.templatesDir });
1013
+ const artifacts = [];
1014
+ for (const skill of skills) {
1015
+ artifacts.push({
1016
+ kind: "skill",
1017
+ name: skill.name,
1018
+ content: skill.content,
1019
+ hash: skill.hash,
1020
+ lastChangedIn: skill.lastChangedIn,
1021
+ destination: installedSkillPath(target.skillsDir, skill.name)
1022
+ });
1023
+ const resources = await listDefaultSkillResources(skill);
1024
+ for (const resource of resources) {
1025
+ artifacts.push({
1026
+ kind: "resource",
1027
+ name: `${skill.name}/${resource.name}`,
1028
+ content: resource.content,
1029
+ hash: resource.hash,
1030
+ lastChangedIn: skill.lastChangedIn,
1031
+ destination: installedSkillResourcePath(target.skillsDir, skill.name, resource.name)
1032
+ });
1033
+ }
1034
+ }
1035
+ if (target.agent === "opencode" && target.commandsDir) {
1036
+ const commands = onlyName ? await maybeReadDefaultOpencodeCommand(onlyName, { commandTemplatesDir: options.commandTemplatesDir }) : await listDefaultOpencodeCommands({ commandTemplatesDir: options.commandTemplatesDir });
1037
+ for (const command of Array.isArray(commands) ? commands : commands ? [commands] : []) {
1038
+ artifacts.push({
1039
+ kind: "command",
1040
+ name: command.name,
1041
+ content: command.content,
1042
+ hash: command.hash,
1043
+ lastChangedIn: null,
1044
+ destination: installedOpencodeCommandPath(target.commandsDir, command.name)
1045
+ });
1046
+ }
1047
+ }
1048
+ return artifacts;
1049
+ }
1050
+ async function listDefaultSkillResources(skill) {
1051
+ const skillDir = dirname2(skill.sourcePath);
1052
+ const entries = await readdir2(skillDir, { withFileTypes: true });
1053
+ const resources = await Promise.all(
1054
+ entries.filter((entry) => entry.isFile() && entry.name !== "SKILL.md").map(async (entry) => {
1055
+ validateSkillResourceName(entry.name);
1056
+ const sourcePath = join(skillDir, entry.name);
1057
+ const content = await readFile5(sourcePath, "utf8");
1058
+ return {
1059
+ name: entry.name,
1060
+ sourcePath,
1061
+ content,
1062
+ hash: hashContent(content)
1063
+ };
1064
+ })
1065
+ );
1066
+ return resources.sort((left, right) => left.name.localeCompare(right.name));
1067
+ }
1068
+ async function listDefaultOpencodeCommands(options = {}) {
1069
+ const commandTemplatesDir = options.commandTemplatesDir ?? await findDefaultOpencodeCommandsDir();
1070
+ const entries = await readdir2(commandTemplatesDir, { withFileTypes: true }).catch((error) => {
1071
+ if (isNodeError(error) && error.code === "ENOENT") {
1072
+ return [];
1073
+ }
1074
+ throw error;
1075
+ });
1076
+ const commands = await Promise.all(
1077
+ entries.filter((entry) => entry.isFile() && extname(entry.name) === ".md").map((entry) => readDefaultOpencodeCommand(basename(entry.name, ".md"), { commandTemplatesDir }))
1078
+ );
1079
+ return commands.sort((left, right) => left.name.localeCompare(right.name));
1080
+ }
1081
+ async function readDefaultOpencodeCommand(name, options = {}) {
1082
+ validateSkillName(name);
1083
+ const commandTemplatesDir = options.commandTemplatesDir ?? await findDefaultOpencodeCommandsDir();
1084
+ const sourcePath = join(commandTemplatesDir, `${name}.md`);
1085
+ const content = await readFile5(sourcePath, "utf8");
1086
+ return {
1087
+ name,
1088
+ sourcePath,
1089
+ content,
1090
+ hash: hashContent(content)
1091
+ };
1092
+ }
1093
+ async function maybeReadDefaultOpencodeCommand(name, options = {}) {
1094
+ return readDefaultOpencodeCommand(name, options).catch((error) => {
1095
+ if (isNodeError(error) && error.code === "ENOENT") {
1096
+ return void 0;
1097
+ }
1098
+ throw error;
1099
+ });
1100
+ }
1101
+ async function installArtifact(cwd, agent, artifact, manifest, now) {
1102
+ const relativePath = relative(cwd, artifact.destination);
1103
+ await ensureDir(dirname2(artifact.destination));
1104
+ if (!await pathExists(artifact.destination)) {
1105
+ await writeFile3(artifact.destination, artifact.content);
1106
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1107
+ return result(agent, artifact, relativePath, "installed", `Installed ${artifact.name} ${artifact.kind} for ${agent}`);
1108
+ }
1109
+ const currentHash = hashContent(await readFile5(artifact.destination, "utf8"));
1110
+ const entry = getManifestEntry(manifest, agent, artifact.kind, artifact.name);
1111
+ if (entry && currentHash !== entry.installed_hash) {
1112
+ return result(agent, artifact, relativePath, "modified", `Skipped modified ${artifact.name} ${artifact.kind} for ${agent}`);
1113
+ }
1114
+ if (!entry && currentHash !== artifact.hash) {
1115
+ return result(agent, artifact, relativePath, "modified", `Skipped existing ${artifact.name} ${artifact.kind} for ${agent}`);
1116
+ }
1117
+ if (entry && currentHash === entry.installed_hash && currentHash !== artifact.hash) {
1118
+ await writeFile3(artifact.destination, artifact.content);
1119
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1120
+ return result(agent, artifact, relativePath, "updated", `Updated ${artifact.name} ${artifact.kind} for ${agent}`);
1121
+ }
1122
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1123
+ return result(agent, artifact, relativePath, "unchanged", `${artifact.name} ${artifact.kind} already installed for ${agent}`);
1124
+ }
1125
+ async function updateArtifact(cwd, agent, artifact, manifest, now) {
1126
+ const relativePath = relative(cwd, artifact.destination);
1127
+ const entry = getManifestEntry(manifest, agent, artifact.kind, artifact.name);
1128
+ if (!await pathExists(artifact.destination)) {
1129
+ if (!entry && artifact.kind === "resource") {
1130
+ await ensureDir(dirname2(artifact.destination));
1131
+ await writeFile3(artifact.destination, artifact.content);
1132
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1133
+ return result(agent, artifact, relativePath, "installed", `Installed ${artifact.name} ${artifact.kind} for ${agent}`);
1134
+ }
1135
+ return result(agent, artifact, relativePath, "missing", `Missing ${artifact.name} ${artifact.kind} for ${agent}`);
1136
+ }
1137
+ const currentHash = hashContent(await readFile5(artifact.destination, "utf8"));
1138
+ if (!entry || currentHash !== entry.installed_hash) {
1139
+ return result(agent, artifact, relativePath, "modified", `Skipped modified ${artifact.name} ${artifact.kind} for ${agent}`);
1140
+ }
1141
+ if (currentHash === artifact.hash) {
1142
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1143
+ return result(agent, artifact, relativePath, "unchanged", `${artifact.name} ${artifact.kind} already up to date for ${agent}`);
1144
+ }
1145
+ await writeFile3(artifact.destination, artifact.content);
1146
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1147
+ return result(agent, artifact, relativePath, "updated", `Updated ${artifact.name} ${artifact.kind} for ${agent}`);
1148
+ }
1149
+ async function resetArtifact(cwd, agent, artifact, manifest, now) {
1150
+ const relativePath = relative(cwd, artifact.destination);
1151
+ await ensureDir(dirname2(artifact.destination));
1152
+ await writeFile3(artifact.destination, artifact.content);
1153
+ setManifestEntry(manifest, agent, artifact, relativePath, now);
1154
+ return result(agent, artifact, relativePath, "reset", `Reset ${artifact.name} ${artifact.kind} for ${agent}`);
1155
+ }
1156
+ function setManifestEntry(manifest, agent, artifact, relativePath, now) {
1157
+ manifest.installed[agent] ??= {};
1158
+ const entries = manifest.installed[agent];
1159
+ if (!entries) {
1160
+ return;
1161
+ }
1162
+ const bucket = manifestBucketForKind(artifact.kind);
1163
+ entries[bucket] ??= {};
1164
+ entries[bucket][artifact.name] = {
1165
+ path: relativePath,
1166
+ source_hash: artifact.hash,
1167
+ installed_hash: artifact.hash,
1168
+ installed_at: now.toISOString(),
1169
+ installed_from: artifact.lastChangedIn
1170
+ };
1171
+ }
1172
+ function getManifestEntry(manifest, agent, kind, name) {
1173
+ const entries = manifest.installed[agent];
1174
+ return entries?.[manifestBucketForKind(kind)]?.[name];
1175
+ }
1176
+ function resolveAgentTargets(cwd, agent) {
1177
+ switch (agent) {
1178
+ case "codex":
1179
+ return [{ agent, skillsDir: join(cwd, ".agents", "skills") }];
1180
+ case "cursor":
1181
+ return [{ agent, skillsDir: join(cwd, ".agents", "skills") }];
1182
+ case "claude":
1183
+ return [{ agent, skillsDir: join(cwd, ".claude", "skills") }];
1184
+ case "opencode":
1185
+ return [{ agent, skillsDir: join(cwd, ".agents", "skills"), commandsDir: join(cwd, ".opencode", "commands") }];
1186
+ case "all":
1187
+ return [
1188
+ { agent: "codex", skillsDir: join(cwd, ".agents", "skills") },
1189
+ { agent: "cursor", skillsDir: join(cwd, ".agents", "skills") },
1190
+ { agent: "claude", skillsDir: join(cwd, ".claude", "skills") },
1191
+ { agent: "opencode", skillsDir: join(cwd, ".agents", "skills"), commandsDir: join(cwd, ".opencode", "commands") }
1192
+ ];
1193
+ default:
1194
+ throw new Error(`Unsupported agent: ${agent}`);
1195
+ }
1196
+ }
1197
+ function installedSkillPath(skillsDir, skillName) {
1198
+ return join(skillsDir, skillName, "SKILL.md");
1199
+ }
1200
+ function installedSkillResourcePath(skillsDir, skillName, resourceName) {
1201
+ return join(skillsDir, skillName, resourceName);
1202
+ }
1203
+ function installedOpencodeCommandPath(commandsDir, commandName) {
1204
+ return join(commandsDir, `${commandName}.md`);
1205
+ }
1206
+ async function loadAgentsManifest(cwd) {
1207
+ const manifestPath = join(cwd, manifestRelativePath);
1208
+ if (!await pathExists(manifestPath)) {
1209
+ return { version: 1, installed: {} };
1210
+ }
1211
+ const parsed = YAML5.parse(await readFile5(manifestPath, "utf8"));
1212
+ const installed = parsed?.installed ?? {};
1213
+ for (const agent of Object.keys(installed)) {
1214
+ const buckets = installed[agent];
1215
+ if (!buckets) continue;
1216
+ for (const bucket of ["skills", "commands", "resources"]) {
1217
+ const entries = buckets[bucket];
1218
+ if (!entries) continue;
1219
+ for (const name of Object.keys(entries)) {
1220
+ const entry = entries[name];
1221
+ entries[name] = {
1222
+ path: entry.path ?? "",
1223
+ source_hash: entry.source_hash ?? "",
1224
+ installed_hash: entry.installed_hash ?? "",
1225
+ installed_at: entry.installed_at ?? "",
1226
+ installed_from: entry.installed_from ?? null
1227
+ };
1228
+ }
1229
+ }
1230
+ }
1231
+ return {
1232
+ version: 1,
1233
+ installed
1234
+ };
1235
+ }
1236
+ async function saveAgentsManifest(cwd, manifest) {
1237
+ const manifestPath = join(cwd, manifestRelativePath);
1238
+ await mkdir2(dirname2(manifestPath), { recursive: true });
1239
+ await writeFileAtomic(manifestPath, YAML5.stringify(manifest));
1240
+ }
1241
+ function parseSkillFrontmatter(content, sourcePath) {
1242
+ const match = /^---\n([\s\S]*?)\n---/.exec(content);
1243
+ if (!match) {
1244
+ throw new Error(`Missing frontmatter in ${sourcePath}`);
1245
+ }
1246
+ const metadata = YAML5.parse(match[1]);
1247
+ if (!metadata.name || !metadata.description) {
1248
+ throw new Error(`Skill frontmatter in ${sourcePath} must include name and description`);
1249
+ }
1250
+ if (metadata.last_changed_in === void 0 || metadata.last_changed_in === null || metadata.last_changed_in === "") {
1251
+ throw new Error(
1252
+ `Skill frontmatter in ${sourcePath} must include last_changed_in (the weave-it package version of the last skill change)`
1253
+ );
1254
+ }
1255
+ return {
1256
+ name: metadata.name,
1257
+ description: metadata.description,
1258
+ lastChangedIn: String(metadata.last_changed_in)
1259
+ };
1260
+ }
1261
+ function formatFullFileDiff(installedPath, defaultName, installed, currentDefault) {
1262
+ if (installed === currentDefault) {
1263
+ return `No differences for ${installedPath}`;
1264
+ }
1265
+ const installedLines = splitLines(installed);
1266
+ const defaultLines = splitLines(currentDefault);
1267
+ return [
1268
+ `--- installed:${installedPath}`,
1269
+ `+++ default:${defaultName}`,
1270
+ ...installedLines.map((line) => `-${line}`),
1271
+ ...defaultLines.map((line) => `+${line}`)
1272
+ ].join("\n");
1273
+ }
1274
+ function splitLines(value) {
1275
+ return value.endsWith("\n") ? value.slice(0, -1).split("\n") : value.split("\n");
1276
+ }
1277
+ function result(agent, artifact, path14, status, message) {
1278
+ return { agent, kind: artifact.kind, skill: artifact.name, path: path14, status, message };
1279
+ }
1280
+ function summarize(results) {
1281
+ return {
1282
+ status: "ok",
1283
+ message: results.map((item) => item.message).join("\n"),
1284
+ results
1285
+ };
1286
+ }
1287
+ function hashContent(content) {
1288
+ return `sha256:${createHash2("sha256").update(content).digest("hex")}`;
1289
+ }
1290
+ async function findDefaultSkillsDir() {
1291
+ return join(await findTemplatesRoot(), "skills");
1292
+ }
1293
+ async function findDefaultOpencodeCommandsDir() {
1294
+ return join(await findTemplatesRoot(), "opencode", "commands");
1295
+ }
1296
+ async function findTemplatesRoot() {
1297
+ let current = dirname2(fileURLToPath(import.meta.url));
1298
+ while (true) {
1299
+ const candidate = join(current, "templates");
1300
+ try {
1301
+ await access2(candidate, constants2.R_OK);
1302
+ return candidate;
1303
+ } catch {
1304
+ const parent = dirname2(current);
1305
+ if (parent === current) {
1306
+ throw new Error("Could not locate templates");
1307
+ }
1308
+ current = parent;
1309
+ }
1310
+ }
1311
+ }
1312
+ function validateSkillName(name) {
1313
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
1314
+ throw new Error(`Invalid skill name: ${name}`);
1315
+ }
1316
+ }
1317
+ function validateSkillResourceName(name) {
1318
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*\.md$/.test(name)) {
1319
+ throw new Error(`Invalid skill resource name: ${name}`);
1320
+ }
1321
+ }
1322
+ function manifestBucketForKind(kind) {
1323
+ switch (kind) {
1324
+ case "skill":
1325
+ return "skills";
1326
+ case "command":
1327
+ return "commands";
1328
+ case "resource":
1329
+ return "resources";
1330
+ default:
1331
+ throw new Error(`Unsupported artifact kind: ${kind}`);
1332
+ }
1333
+ }
1334
+ function isNodeError(error) {
1335
+ return error instanceof Error && "code" in error;
1336
+ }
1337
+
1338
+ // src/commands/agent.ts
1339
+ function agentCommand() {
1340
+ const command = new Command2("agent").description("Install and manage agent skills.");
1341
+ command.command("install").description("Install Weave skills for an agent.").argument("<agent>", "codex, cursor, claude, opencode, or all", parseAgentSelection).option("--json", "print machine-readable JSON").action(async (agent, options) => {
1342
+ await runAction(async () => {
1343
+ const result3 = await installAgentSkills({ cwd: process.cwd(), agent });
1344
+ writeOperationResult(result3.message, result3.results, options.json ?? false);
1345
+ });
1346
+ });
1347
+ command.command("update").description("Update installed Weave skills when they have not been modified.").argument("<agent>", "codex, cursor, claude, opencode, or all", parseAgentSelection).option("--json", "print machine-readable JSON").action(async (agent, options) => {
1348
+ await runAction(async () => {
1349
+ const result3 = await updateAgentSkills({ cwd: process.cwd(), agent });
1350
+ writeOperationResult(result3.message, result3.results, options.json ?? false);
1351
+ });
1352
+ });
1353
+ command.command("diff").description("Show differences between installed skills and current Weave defaults.").argument("<agent>", "codex, cursor, claude, opencode, or all", parseAgentSelection).argument("[skill]", "skill name").action(async (agent, skill) => {
1354
+ await runAction(async () => {
1355
+ const result3 = await diffAgentSkills({ cwd: process.cwd(), agent, skill });
1356
+ process.stdout.write(`${result3.message}
1357
+ `);
1358
+ });
1359
+ });
1360
+ command.command("reset").description("Overwrite installed skills with current Weave defaults.").argument("<agent>", "codex, cursor, claude, opencode, or all", parseAgentSelection).argument("[skill]", "skill name").option("--json", "print machine-readable JSON").action(async (agent, skill, options) => {
1361
+ await runAction(async () => {
1362
+ const result3 = await resetAgentSkills({ cwd: process.cwd(), agent, skill });
1363
+ writeOperationResult(result3.message, result3.results, options.json ?? false);
1364
+ });
1365
+ });
1366
+ return command;
1367
+ }
1368
+ function parseAgentSelection(value) {
1369
+ if (value === "codex" || value === "cursor" || value === "claude" || value === "opencode" || value === "all") {
1370
+ return value;
1371
+ }
1372
+ throw new InvalidArgumentError(`Unsupported agent: ${value}`);
1373
+ }
1374
+ function writeOperationResult(message, results, json) {
1375
+ if (json) {
1376
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
1377
+ `);
1378
+ return;
1379
+ }
1380
+ process.stdout.write(`${message}
1381
+ `);
1382
+ }
1383
+ async function runAction(action) {
1384
+ try {
1385
+ await action();
1386
+ } catch (error) {
1387
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
1388
+ `);
1389
+ process.exitCode = 1;
1390
+ }
1391
+ }
1392
+
1393
+ // src/commands/artifact.ts
1394
+ import { Command as Command3, InvalidArgumentError as InvalidArgumentError2 } from "commander";
1395
+
1396
+ // src/lib/artifact-context.ts
1397
+ import path9 from "path";
1398
+
1399
+ // src/lib/architecture-artifact.ts
1400
+ import { readFile as readFile6, readdir as readdir3, stat as stat3 } from "fs/promises";
1401
+ import path7 from "path";
1402
+ async function resolveArchitectureArtifact(changePath) {
1403
+ const filePath = path7.join(changePath, "architecture.md");
1404
+ const folderPath = path7.join(changePath, "architecture");
1405
+ const fileExists = await pathExists(filePath);
1406
+ const folderExists = await isDirectory(folderPath);
1407
+ if (fileExists && folderExists) {
1408
+ const folder = await readFolderArchitecture(folderPath);
1409
+ const fileSubstantive = await hasSubstantiveMarkdown(filePath);
1410
+ const substantive = fileSubstantive || folder.indexSubstantive || folder.substantiveFacetPaths.length > 0;
1411
+ return {
1412
+ status: "conflict",
1413
+ filePath,
1414
+ folderPath,
1415
+ substantive,
1416
+ fileSubstantive,
1417
+ ...folder
1418
+ };
1419
+ }
1420
+ if (fileExists) {
1421
+ return {
1422
+ status: "file",
1423
+ path: filePath,
1424
+ filePath,
1425
+ folderPath,
1426
+ substantive: await hasSubstantiveMarkdown(filePath)
1427
+ };
1428
+ }
1429
+ if (folderExists) {
1430
+ const folder = await readFolderArchitecture(folderPath);
1431
+ return {
1432
+ status: "folder",
1433
+ filePath,
1434
+ folderPath,
1435
+ ...folder,
1436
+ substantive: folder.indexSubstantive || folder.substantiveFacetPaths.length > 0,
1437
+ partial: !folder.indexSubstantive && folder.substantiveFacetPaths.length > 0
1438
+ };
1439
+ }
1440
+ return {
1441
+ status: "missing",
1442
+ filePath,
1443
+ folderPath,
1444
+ substantive: false
1445
+ };
1446
+ }
1447
+ async function hasSubstantiveMarkdown(filePath) {
1448
+ if (!await pathExists(filePath)) {
1449
+ return false;
1450
+ }
1451
+ const content = await readFile6(filePath, "utf8");
1452
+ const withoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
1453
+ const withoutScaffold = withoutFrontmatter.split("\n").filter((line) => {
1454
+ const trimmed = line.trim();
1455
+ return trimmed && !trimmed.startsWith("#") && trimmed !== "Not ready";
1456
+ }).join("\n").trim();
1457
+ return withoutScaffold.length > 0;
1458
+ }
1459
+ async function readFolderArchitecture(folderPath) {
1460
+ const indexPath = path7.join(folderPath, "index.md");
1461
+ const entries = await readdir3(folderPath, { withFileTypes: true });
1462
+ const markdownPaths = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => path7.join(folderPath, entry.name)).sort((left, right) => left.localeCompare(right));
1463
+ const facetPaths = markdownPaths.filter((filePath) => path7.basename(filePath) !== "index.md");
1464
+ const substantiveFacetPaths = [];
1465
+ for (const facetPath of facetPaths) {
1466
+ if (await hasSubstantiveMarkdown(facetPath)) {
1467
+ substantiveFacetPaths.push(facetPath);
1468
+ }
1469
+ }
1470
+ return {
1471
+ indexPath,
1472
+ indexExists: await pathExists(indexPath),
1473
+ indexSubstantive: await hasSubstantiveMarkdown(indexPath),
1474
+ facetPaths,
1475
+ substantiveFacetPaths
1476
+ };
1477
+ }
1478
+ async function isDirectory(filePath) {
1479
+ try {
1480
+ return (await stat3(filePath)).isDirectory();
1481
+ } catch {
1482
+ return false;
1483
+ }
1484
+ }
1485
+
1486
+ // src/lib/artifact-metadata.ts
1487
+ import YAML6 from "yaml";
1488
+ var artifactNames = ["exploration", "prd", "architecture"];
1489
+ function isArtifactName(value) {
1490
+ return Boolean(value && artifactNames.includes(value));
1491
+ }
1492
+ function artifactFileName(artifact) {
1493
+ return artifact === "exploration" ? "exploration.md" : `${artifact}.md`;
1494
+ }
1495
+ function defaultArtifactOwner(artifact) {
1496
+ return artifact === "architecture" ? "engineering" : "product";
1497
+ }
1498
+ function defaultArtifactSource(artifact) {
1499
+ if (artifact === "exploration") {
1500
+ return "discussion";
1501
+ }
1502
+ return artifact === "prd" ? "exploration.md" : "prd.md";
1503
+ }
1504
+ function artifactFrontmatter(options) {
1505
+ const artifact = options.artifact;
1506
+ const timestamp = options.now.toISOString();
1507
+ return `---
1508
+ ${YAML6.stringify({
1509
+ artifact,
1510
+ status: options.status ?? "draft",
1511
+ owner: options.owner ?? defaultArtifactOwner(artifact),
1512
+ created_at: timestamp,
1513
+ updated_at: timestamp,
1514
+ reviewed_at: null,
1515
+ approved_at: null,
1516
+ approved_by: null,
1517
+ source: options.source ?? defaultArtifactSource(artifact)
1518
+ })}---
1519
+
1520
+ `;
1521
+ }
1522
+
1523
+ // src/lib/changes.ts
1524
+ import { randomBytes } from "crypto";
1525
+ import { execFile as execFile2 } from "child_process";
1526
+ import { mkdir as mkdir3, readFile as readFile7, readdir as readdir4, writeFile as writeFile4 } from "fs/promises";
1527
+ import path8 from "path";
1528
+ import { promisify as promisify2 } from "util";
1529
+ import YAML7 from "yaml";
1530
+ var execFileAsync2 = promisify2(execFile2);
1531
+ var idChars = "abcdefghijklmnopqrstuvwxyz0123456789";
1532
+ var changeTypes = ["feat", "fix", "refactor", "docs", "test", "ci", "chore"];
1533
+ var changeStages = ["exploration", "prd", "architecture", "issues"];
1534
+ var storedStages = ["started", ...changeStages];
1535
+ var artifactSourceIds = ["exploration", "prd", "architecture", "discussion", "sessions", "codebase"];
1536
+ var knowledgeStatuses = ["pending", "stale", "updated", "none"];
1537
+ var ChangeCommandError = class extends Error {
1538
+ constructor(code, message, details) {
1539
+ super(message);
1540
+ this.code = code;
1541
+ this.details = details;
1542
+ }
1543
+ code;
1544
+ details;
1545
+ };
1546
+ async function createChange(options) {
1547
+ const now = options.now ?? /* @__PURE__ */ new Date();
1548
+ const title = options.title.trim();
1549
+ if (!title) {
1550
+ throw new ChangeCommandError("missing_title", "Change title is required");
1551
+ }
1552
+ const targets = [await resolveTarget(options.cwd, options.sessionPath)];
1553
+ const type = options.type ?? "feat";
1554
+ const slug = normalizeChangeSlug(options.slug ?? title);
1555
+ const id = await generateChangeId(targets, slug, now, options.randomId ?? randomChangeIdPart);
1556
+ const branch = changeBranch(id);
1557
+ await assertChangeMissing(targets, id);
1558
+ const results = [];
1559
+ for (const target of targets) {
1560
+ const branchStatus = await ensureChangeBranch(target.path, branch);
1561
+ await ensureWeaveScaffold({ folder: { path: target.path } });
1562
+ const changePath = changeDir(target.path, id);
1563
+ await mkdir3(changePath, { recursive: false });
1564
+ await mkdir3(path8.join(changePath, "sessions"));
1565
+ await writeFile4(path8.join(changePath, "status.yml"), statusTemplate({ id, slug, title, type, branch, now }));
1566
+ if (type === "feat") {
1567
+ await writeFile4(path8.join(changePath, "exploration.md"), explorationTemplate(title, title, now));
1568
+ }
1569
+ results.push({ path: target.path, changePath, branch, branchStatus, current: true });
1570
+ }
1571
+ await saveCurrentForTargets(
1572
+ options.sessionPath,
1573
+ results.map((target) => ({
1574
+ root: target.path,
1575
+ changeId: id,
1576
+ changePath: target.changePath,
1577
+ branch,
1578
+ artifact: type === "feat" ? "exploration" : void 0
1579
+ })),
1580
+ now
1581
+ );
1582
+ return summarizeChangeOperation({ id, slug, title, type, branch, targets: results, verb: "Created" });
1583
+ }
1584
+ async function listChanges(options) {
1585
+ const session = await loadCurrentSession(options.sessionPath ?? defaultSessionPath());
1586
+ const targets = [await resolveTarget(options.cwd, options.sessionPath)];
1587
+ const summaries = [];
1588
+ for (const target of targets) {
1589
+ const activeId = activeChangeForTarget(session, target)?.id;
1590
+ const changes = await readChanges(target.path, activeId);
1591
+ summaries.push({ id: target.id, name: target.name, path: target.path, changes });
1592
+ }
1593
+ return {
1594
+ status: "ok",
1595
+ targets: summaries,
1596
+ message: formatListMessage(summaries)
1597
+ };
1598
+ }
1599
+ async function currentChange(options) {
1600
+ const now = options.now ?? /* @__PURE__ */ new Date();
1601
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
1602
+ const session = await loadOrCreateSession(sessionPath, now);
1603
+ const targets = [await resolveTarget(options.cwd, sessionPath)];
1604
+ const results = [];
1605
+ let mutated = false;
1606
+ for (const target of targets) {
1607
+ const context = await currentContextForTarget(session, target, now, { saveInferred: true });
1608
+ mutated ||= context.saved;
1609
+ results.push(context);
1610
+ }
1611
+ if (mutated) {
1612
+ await saveCurrentSession(session, sessionPath);
1613
+ }
1614
+ return {
1615
+ status: "ok",
1616
+ targets: results,
1617
+ message: formatCurrentMessage(results)
1618
+ };
1619
+ }
1620
+ async function statusChange(options) {
1621
+ const now = options.now ?? /* @__PURE__ */ new Date();
1622
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
1623
+ const session = await loadOrCreateSession(sessionPath, now);
1624
+ const targets = [await resolveTarget(options.cwd, sessionPath)];
1625
+ const results = [];
1626
+ let mutated = false;
1627
+ for (const target of targets) {
1628
+ if (options.change) {
1629
+ const changes = await readChanges(target.path, activeChangeForTarget(session, target)?.id);
1630
+ const change = resolveChangeReference(changes, options.change);
1631
+ const branch = await currentBranch2(target.path);
1632
+ results.push({
1633
+ id: target.id,
1634
+ name: target.name,
1635
+ path: target.path,
1636
+ active: change.active,
1637
+ source: "explicit",
1638
+ saved: false,
1639
+ change,
1640
+ branch,
1641
+ branchMatch: branchMatch(branch, change.branch, await findGitRoot(target.path))
1642
+ });
1643
+ continue;
1644
+ }
1645
+ const context = await currentContextForTarget(session, target, now, { saveInferred: true });
1646
+ mutated ||= context.saved;
1647
+ results.push({
1648
+ id: context.id,
1649
+ name: context.name,
1650
+ path: context.path,
1651
+ active: Boolean(context.current),
1652
+ source: context.source,
1653
+ saved: context.saved,
1654
+ change: context.current,
1655
+ branch: context.branch,
1656
+ branchMatch: context.branchMatch,
1657
+ mismatch: context.mismatch
1658
+ });
1659
+ }
1660
+ if (mutated) {
1661
+ await saveCurrentSession(session, sessionPath);
1662
+ }
1663
+ return {
1664
+ status: "ok",
1665
+ targets: results,
1666
+ message: formatStatusMessage(results)
1667
+ };
1668
+ }
1669
+ async function activeChangeContext(options) {
1670
+ const now = options.now ?? /* @__PURE__ */ new Date();
1671
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
1672
+ const session = await loadOrCreateSession(sessionPath, now);
1673
+ const context = await resolveChangeContext(options.cwd, sessionPath);
1674
+ if (!context) {
1675
+ throw new ChangeCommandError("no_weave_context", "No Weave context found. Run `weave init` first.");
1676
+ }
1677
+ const target = {
1678
+ id: context.folderId,
1679
+ name: context.folderName,
1680
+ path: context.rootPath
1681
+ };
1682
+ const current = await currentContextForTarget(session, target, now, { saveInferred: true });
1683
+ if (current.saved) {
1684
+ await saveCurrentSession(session, sessionPath);
1685
+ }
1686
+ if (!current.current) {
1687
+ throw new ChangeCommandError("no_current_change", "No active Weave change found. Run `weave change new` or `weave change switch` first.");
1688
+ }
1689
+ return {
1690
+ target: { id: target.id, name: target.name, path: target.path },
1691
+ change: current.current,
1692
+ mode: context.mode,
1693
+ branch: current.branch,
1694
+ branchMatch: current.branchMatch
1695
+ };
1696
+ }
1697
+ async function switchChange(options) {
1698
+ const now = options.now ?? /* @__PURE__ */ new Date();
1699
+ const target = await resolveTarget(options.cwd, options.sessionPath);
1700
+ await assertCleanGitTargets([target]);
1701
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
1702
+ const session = await loadOrCreateSession(sessionPath, now);
1703
+ const changes = await readChanges(target.path, activeChangeForTarget(session, target)?.id);
1704
+ const change = resolveChangeReference(changes, options.change);
1705
+ const branchStatus = await ensureChangeBranch(target.path, change.branch);
1706
+ setCurrentChangeForPath(
1707
+ session,
1708
+ target.path,
1709
+ { id: change.id, path: change.path, branch: change.branch },
1710
+ now
1711
+ );
1712
+ const activeArtifact = activeArtifactForTarget(session, target);
1713
+ if (activeArtifact && activeArtifact.change_id !== change.id) {
1714
+ clearCurrentArtifactForPath(session, target.path, now);
1715
+ }
1716
+ await saveCurrentSession(session, sessionPath);
1717
+ return {
1718
+ status: "ok",
1719
+ change: { ...change, active: true },
1720
+ branchStatus,
1721
+ target: { id: target.id, name: target.name, path: target.path },
1722
+ message: [
1723
+ `Switched change: ${change.id}`,
1724
+ "",
1725
+ `Title: ${change.title}`,
1726
+ `Type: ${change.type}`,
1727
+ `Stage: ${change.stage}`,
1728
+ `Branch: ${change.branch} (${formatBranchStatus(branchStatus)})`,
1729
+ `Path: ${change.path}`,
1730
+ "Current: yes"
1731
+ ].join("\n")
1732
+ };
1733
+ }
1734
+ async function progressChange(options) {
1735
+ const now = options.now ?? /* @__PURE__ */ new Date();
1736
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
1737
+ const session = await loadOrCreateSession(sessionPath, now);
1738
+ const target = await resolveTarget(options.cwd, sessionPath);
1739
+ const context = await currentContextForTarget(session, target, now, { saveInferred: true });
1740
+ if (context.saved) {
1741
+ await saveCurrentSession(session, sessionPath);
1742
+ }
1743
+ if (!context.current) {
1744
+ throw new ChangeCommandError("no_current_change", "No active Weave change found. Run `weave change new` or `weave change switch` first.");
1745
+ }
1746
+ const statusPath = path8.join(context.current.changePath, "status.yml");
1747
+ const raw = await readStatusFile(statusPath);
1748
+ const existing = await readChangeMetadata(context.current.changePath, context.current.id);
1749
+ const sourceResolution = await resolveProgressSources(options, context.current.changePath);
1750
+ const nextStage = maxStage([existing.stage, options.stage]);
1751
+ const artifacts = {
1752
+ ...existing.artifacts,
1753
+ [options.stage]: {
1754
+ sources: sourceResolution.sources,
1755
+ updated_at: now.toISOString()
1756
+ }
1757
+ };
1758
+ const stale = { ...existing.stale };
1759
+ delete stale[options.stage];
1760
+ const invalidatedAt = now.toISOString();
1761
+ const computedDependents = transitiveDependents(options.stage, artifacts);
1762
+ const propagationTargets = resolveStalePropagationTargets({
1763
+ computed: computedDependents,
1764
+ noInvalidate: options.noInvalidate ?? false,
1765
+ invalidateOnly: options.invalidateOnly
1766
+ });
1767
+ for (const stage of propagationTargets) {
1768
+ stale[stage] = {
1769
+ invalidated_by: options.stage,
1770
+ invalidated_at: invalidatedAt
1771
+ };
1772
+ }
1773
+ const nextStatus = {
1774
+ ...raw,
1775
+ stage: nextStage,
1776
+ artifacts,
1777
+ updated_at: invalidatedAt
1778
+ };
1779
+ if (Object.keys(stale).length > 0) {
1780
+ Object.assign(nextStatus, { stale });
1781
+ } else {
1782
+ delete nextStatus.stale;
1783
+ }
1784
+ const knowledge = staleKnowledgeFromProgress(raw.knowledge, existing.knowledge, options.stage, invalidatedAt);
1785
+ if (knowledge) {
1786
+ Object.assign(nextStatus, { knowledge });
1787
+ }
1788
+ await writeFile4(statusPath, YAML7.stringify(nextStatus));
1789
+ const updated = await readChangeMetadata(context.current.changePath, context.current.id);
1790
+ const change = {
1791
+ ...updated,
1792
+ path: context.current.path,
1793
+ changePath: context.current.changePath,
1794
+ active: true
1795
+ };
1796
+ return {
1797
+ status: "ok",
1798
+ target: { id: target.id, name: target.name, path: target.path },
1799
+ change,
1800
+ progressed: options.stage,
1801
+ sources: sourceResolution.sources,
1802
+ note: sourceResolution.note,
1803
+ message: formatProgressMessage({ id: target.id, name: target.name, path: target.path }, change, options.stage, sourceResolution.note)
1804
+ };
1805
+ }
1806
+ async function clearChangeStaleness(options) {
1807
+ const now = options.now ?? /* @__PURE__ */ new Date();
1808
+ const session = await loadOrCreateSession(
1809
+ options.sessionPath ?? defaultSessionPath(),
1810
+ now
1811
+ );
1812
+ const target = await resolveTarget(options.cwd, options.sessionPath);
1813
+ const context = await currentContextForTarget(session, target, now, { saveInferred: true });
1814
+ if (context.saved) {
1815
+ await saveCurrentSession(session, options.sessionPath ?? defaultSessionPath());
1816
+ }
1817
+ if (!context.current) {
1818
+ throw new ChangeCommandError(
1819
+ "no_current_change",
1820
+ "No active Weave change found. Run `weave change new` or `weave change switch` first."
1821
+ );
1822
+ }
1823
+ const statusPath = path8.join(context.current.changePath, "status.yml");
1824
+ const raw = await readStatusFile(statusPath);
1825
+ const existing = await readChangeMetadata(context.current.changePath, context.current.id);
1826
+ const currentStale = existing.stale[options.lane];
1827
+ if (!currentStale) {
1828
+ throw new ChangeCommandError(
1829
+ "lane_not_stale",
1830
+ `Lane "${options.lane}" is not currently marked stale; nothing to clear.`,
1831
+ { lane: options.lane }
1832
+ );
1833
+ }
1834
+ const nextStale = { ...existing.stale };
1835
+ delete nextStale[options.lane];
1836
+ const historyEntry = {
1837
+ lane: options.lane,
1838
+ invalidated_by: currentStale.invalidated_by,
1839
+ invalidated_at: currentStale.invalidated_at,
1840
+ cleared_at: now.toISOString(),
1841
+ reason: options.reason && options.reason.trim().length > 0 ? options.reason.trim() : null
1842
+ };
1843
+ const nextHistory = [...existing.stale_history, historyEntry];
1844
+ const nextStatus = {
1845
+ ...raw,
1846
+ updated_at: now.toISOString(),
1847
+ stale_history: nextHistory
1848
+ };
1849
+ if (Object.keys(nextStale).length > 0) {
1850
+ nextStatus.stale = nextStale;
1851
+ } else {
1852
+ delete nextStatus.stale;
1853
+ }
1854
+ await writeFile4(statusPath, YAML7.stringify(nextStatus));
1855
+ const updated = await readChangeMetadata(context.current.changePath, context.current.id);
1856
+ const change = {
1857
+ ...updated,
1858
+ path: context.current.path,
1859
+ changePath: context.current.changePath,
1860
+ active: true
1861
+ };
1862
+ return {
1863
+ status: "ok",
1864
+ target: { id: target.id, name: target.name, path: target.path },
1865
+ change,
1866
+ cleared: options.lane,
1867
+ history_entry: historyEntry,
1868
+ message: `Cleared stale flag for ${options.lane} (was invalidated by ${currentStale.invalidated_by}).${historyEntry.reason ? ` Reason: ${historyEntry.reason}` : ""}`
1869
+ };
1870
+ }
1871
+ async function knowledgeChange(options) {
1872
+ const now = options.now ?? /* @__PURE__ */ new Date();
1873
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
1874
+ const session = await loadOrCreateSession(sessionPath, now);
1875
+ const target = await resolveTarget(options.cwd, sessionPath);
1876
+ const context = await currentContextForTarget(session, target, now, { saveInferred: true });
1877
+ if (context.saved) {
1878
+ await saveCurrentSession(session, sessionPath);
1879
+ }
1880
+ if (!context.current) {
1881
+ throw new ChangeCommandError("no_current_change", "No active Weave change found. Run `weave change new` or `weave change switch` first.");
1882
+ }
1883
+ const invalidatedBy = options.invalidatedBy ? normalizeKnowledgeInvalidationSource(options.invalidatedBy) : void 0;
1884
+ const statusPath = path8.join(context.current.changePath, "status.yml");
1885
+ const raw = await readStatusFile(statusPath);
1886
+ const existing = await readChangeMetadata(context.current.changePath, context.current.id);
1887
+ const knowledge = mergeKnowledgeMetadata(raw.knowledge, existing.knowledge, {
1888
+ status: options.status,
1889
+ updatedAt: now.toISOString(),
1890
+ domains: options.domains,
1891
+ shared: options.shared,
1892
+ files: options.files,
1893
+ delta: options.delta,
1894
+ reason: options.reason,
1895
+ invalidatedBy
1896
+ });
1897
+ const nextStatus = {
1898
+ ...raw,
1899
+ knowledge,
1900
+ updated_at: now.toISOString()
1901
+ };
1902
+ await writeFile4(statusPath, YAML7.stringify(nextStatus));
1903
+ const updated = await readChangeMetadata(context.current.changePath, context.current.id);
1904
+ const change = {
1905
+ ...updated,
1906
+ path: context.current.path,
1907
+ changePath: context.current.changePath,
1908
+ active: true
1909
+ };
1910
+ if (!change.knowledge) {
1911
+ throw new ChangeCommandError("invalid_knowledge_status", "Knowledge status could not be recorded");
1912
+ }
1913
+ return {
1914
+ status: "ok",
1915
+ target: { id: target.id, name: target.name, path: target.path },
1916
+ change,
1917
+ knowledge: change.knowledge,
1918
+ message: formatKnowledgeMessage({ id: target.id, name: target.name, path: target.path }, change)
1919
+ };
1920
+ }
1921
+ function normalizeChangeSlug(value) {
1922
+ const slug = slugify(value, "change");
1923
+ return slug.split("-").filter(Boolean).slice(0, 6).join("-") || "change";
1924
+ }
1925
+ async function generateChangeId(targets, slug, now, randomId) {
1926
+ const date = formatDatePrefix(now);
1927
+ for (let index = 0; index < 20; index += 1) {
1928
+ const id = `${date}-${randomId()}-${slug}`;
1929
+ const exists = await Promise.all(targets.map((target) => pathExists(changeDir(target.path, id))));
1930
+ if (!exists.some(Boolean)) {
1931
+ return id;
1932
+ }
1933
+ }
1934
+ throw new ChangeCommandError("id_collision", "Could not generate a unique change id");
1935
+ }
1936
+ function formatDatePrefix(now) {
1937
+ const year = String(now.getFullYear()).slice(-2);
1938
+ const month = String(now.getMonth() + 1).padStart(2, "0");
1939
+ const day = String(now.getDate()).padStart(2, "0");
1940
+ return `${year}${month}${day}`;
1941
+ }
1942
+ function randomChangeIdPart() {
1943
+ const bytes = randomBytes(4);
1944
+ return Array.from(bytes, (byte) => idChars[byte % idChars.length]).join("");
1945
+ }
1946
+ function changeBranch(changeId) {
1947
+ return `change/${changeId}`;
1948
+ }
1949
+ function changeDir(root, changeId) {
1950
+ return path8.join(root, "wiki", "changes", changeId);
1951
+ }
1952
+ async function assertChangeMissing(targets, changeId) {
1953
+ const existing = [];
1954
+ for (const target of targets) {
1955
+ const targetChangePath = changeDir(target.path, changeId);
1956
+ if (await pathExists(targetChangePath)) {
1957
+ existing.push(targetChangePath);
1958
+ }
1959
+ }
1960
+ if (existing.length > 0) {
1961
+ throw new ChangeCommandError(
1962
+ "change_exists",
1963
+ `Change already exists:
1964
+ ${existing.map((item) => ` ${item}`).join("\n")}`,
1965
+ { existing }
1966
+ );
1967
+ }
1968
+ }
1969
+ async function resolveTarget(cwd, sessionPath) {
1970
+ const context = await resolveChangeContext(cwd, sessionPath ?? defaultSessionPath());
1971
+ if (!context) {
1972
+ throw new ChangeCommandError("no_weave_context", "No Weave context found. Run `weave init` first.");
1973
+ }
1974
+ return {
1975
+ id: context.folderId,
1976
+ name: context.folderName,
1977
+ path: context.rootPath
1978
+ };
1979
+ }
1980
+ async function readChanges(root, activeId) {
1981
+ const changesRoot = path8.join(root, "wiki", "changes");
1982
+ const entries = await readdir4(changesRoot, { withFileTypes: true }).catch((error) => {
1983
+ if (isNodeError2(error) && error.code === "ENOENT") {
1984
+ return [];
1985
+ }
1986
+ throw error;
1987
+ });
1988
+ const changes = await Promise.all(
1989
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1990
+ const changePath = path8.join(changesRoot, entry.name);
1991
+ const metadata = await readChangeMetadata(changePath, entry.name);
1992
+ return {
1993
+ ...metadata,
1994
+ path: path8.join("wiki", "changes", metadata.id),
1995
+ changePath,
1996
+ active: metadata.id === activeId
1997
+ };
1998
+ })
1999
+ );
2000
+ return changes.sort(compareChangesNewestFirst);
2001
+ }
2002
+ async function readChangeMetadata(changePath, fallbackId) {
2003
+ const statusPath = path8.join(changePath, "status.yml");
2004
+ const parsed = await pathExists(statusPath) ? await readStatusFile(statusPath) : {};
2005
+ const id = typeof parsed?.id === "string" ? parsed.id : fallbackId;
2006
+ const slug = typeof parsed?.slug === "string" ? parsed.slug : id.split("-").slice(2).join("-");
2007
+ const title = typeof parsed?.title === "string" ? parsed.title : titleFromSlug(slug);
2008
+ const type = isChangeType(parsed?.type) ? parsed.type : "feat";
2009
+ const branch = typeof parsed?.branch === "string" ? parsed.branch : changeBranch(id);
2010
+ const stage = isStoredChangeStage(parsed?.stage) ? parsed.stage : "exploration";
2011
+ return {
2012
+ id,
2013
+ slug,
2014
+ title,
2015
+ type,
2016
+ stage,
2017
+ stale: parseStaleLanes(parsed?.stale),
2018
+ stale_history: parseStaleHistory(parsed?.stale_history),
2019
+ artifacts: parseArtifactsMetadata(parsed?.artifacts),
2020
+ knowledge: parseKnowledgeMetadata(parsed?.knowledge),
2021
+ branch,
2022
+ created_at: typeof parsed?.created_at === "string" ? parsed.created_at : void 0,
2023
+ updated_at: typeof parsed?.updated_at === "string" ? parsed.updated_at : void 0
2024
+ };
2025
+ }
2026
+ async function readStatusFile(statusPath) {
2027
+ const parsed = YAML7.parse(await readFile7(statusPath, "utf8"));
2028
+ return isRecord(parsed) ? parsed : {};
2029
+ }
2030
+ function parseStaleHistory(value) {
2031
+ if (!Array.isArray(value)) return [];
2032
+ const out = [];
2033
+ for (const entry of value) {
2034
+ if (!isRecord(entry)) continue;
2035
+ const lane = entry.lane;
2036
+ const clearedAt = entry.cleared_at;
2037
+ if (!isChangeStage(lane) || typeof clearedAt !== "string") continue;
2038
+ out.push({
2039
+ lane,
2040
+ invalidated_by: isChangeStage(entry.invalidated_by) ? entry.invalidated_by : null,
2041
+ invalidated_at: typeof entry.invalidated_at === "string" ? entry.invalidated_at : null,
2042
+ cleared_at: clearedAt,
2043
+ reason: typeof entry.reason === "string" ? entry.reason : null
2044
+ });
2045
+ }
2046
+ return out;
2047
+ }
2048
+ function parseStaleLanes(value) {
2049
+ if (!isRecord(value)) {
2050
+ return {};
2051
+ }
2052
+ const stale = {};
2053
+ for (const [lane, metadata] of Object.entries(value)) {
2054
+ if (!isChangeStage(lane) || !isRecord(metadata)) {
2055
+ continue;
2056
+ }
2057
+ const invalidatedBy = metadata.invalidated_by;
2058
+ const invalidatedAt = metadata.invalidated_at;
2059
+ if (!isChangeStage(invalidatedBy) || typeof invalidatedAt !== "string") {
2060
+ continue;
2061
+ }
2062
+ stale[lane] = {
2063
+ invalidated_by: invalidatedBy,
2064
+ invalidated_at: invalidatedAt
2065
+ };
2066
+ }
2067
+ return stale;
2068
+ }
2069
+ function parseArtifactsMetadata(value) {
2070
+ if (!isRecord(value)) {
2071
+ return {};
2072
+ }
2073
+ const artifacts = {};
2074
+ for (const [lane, metadata] of Object.entries(value)) {
2075
+ if (!isChangeStage(lane) || !isRecord(metadata)) {
2076
+ continue;
2077
+ }
2078
+ const updatedAt = metadata.updated_at;
2079
+ if (typeof updatedAt !== "string") {
2080
+ continue;
2081
+ }
2082
+ artifacts[lane] = {
2083
+ sources: parseArtifactSources(metadata.sources),
2084
+ updated_at: updatedAt
2085
+ };
2086
+ }
2087
+ return artifacts;
2088
+ }
2089
+ function parseArtifactSources(value) {
2090
+ if (!Array.isArray(value)) {
2091
+ return [];
2092
+ }
2093
+ return dedupeSources(value.filter(isArtifactSourceId));
2094
+ }
2095
+ function parseKnowledgeMetadata(value) {
2096
+ if (!isRecord(value) || !isKnowledgeStatus(value.status) || typeof value.updated_at !== "string") {
2097
+ return void 0;
2098
+ }
2099
+ const invalidatedBy = value.invalidated_by;
2100
+ const invalidatedAt = value.invalidated_at;
2101
+ const knowledge = {
2102
+ status: value.status,
2103
+ updated_at: value.updated_at,
2104
+ domains: parseStringList(value.domains),
2105
+ shared: parseStringList(value.shared),
2106
+ files: parseStringList(value.files)
2107
+ };
2108
+ if (typeof value.delta === "string") {
2109
+ knowledge.delta = value.delta;
2110
+ }
2111
+ if (typeof value.reason === "string") {
2112
+ knowledge.reason = value.reason;
2113
+ }
2114
+ if (isKnowledgeInvalidationSource(invalidatedBy) && typeof invalidatedAt === "string") {
2115
+ knowledge.invalidated_by = invalidatedBy;
2116
+ knowledge.invalidated_at = invalidatedAt;
2117
+ }
2118
+ return knowledge;
2119
+ }
2120
+ function parseStringList(value) {
2121
+ if (!Array.isArray(value)) {
2122
+ return [];
2123
+ }
2124
+ return dedupeStrings(value.filter((item) => typeof item === "string" && item.trim().length > 0));
2125
+ }
2126
+ function dedupeStrings(values) {
2127
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
2128
+ }
2129
+ function mergeKnowledgeMetadata(rawKnowledge, existing, update) {
2130
+ const base = isRecord(rawKnowledge) ? { ...rawKnowledge } : {};
2131
+ const currentDomains = existing?.domains ?? parseStringList(base.domains);
2132
+ const currentShared = existing?.shared ?? parseStringList(base.shared);
2133
+ const currentFiles = existing?.files ?? parseStringList(base.files);
2134
+ const next = {
2135
+ ...base,
2136
+ status: update.status,
2137
+ updated_at: update.updatedAt,
2138
+ domains: update.domains ? dedupeStrings(update.domains) : currentDomains,
2139
+ shared: update.shared ? dedupeStrings(update.shared) : currentShared,
2140
+ files: update.files ? dedupeStrings(update.files) : currentFiles
2141
+ };
2142
+ if (update.delta !== void 0) {
2143
+ Object.assign(next, { delta: update.delta });
2144
+ } else if (existing?.delta) {
2145
+ Object.assign(next, { delta: existing.delta });
2146
+ }
2147
+ if (update.reason !== void 0) {
2148
+ Object.assign(next, { reason: update.reason });
2149
+ } else if (existing?.reason) {
2150
+ Object.assign(next, { reason: existing.reason });
2151
+ }
2152
+ if (update.status === "stale") {
2153
+ Object.assign(next, { invalidated_at: update.updatedAt });
2154
+ if (update.invalidatedBy) {
2155
+ Object.assign(next, { invalidated_by: update.invalidatedBy });
2156
+ } else if (existing?.invalidated_by) {
2157
+ Object.assign(next, { invalidated_by: existing.invalidated_by });
2158
+ }
2159
+ } else {
2160
+ delete next.invalidated_by;
2161
+ delete next.invalidated_at;
2162
+ }
2163
+ return parseKnowledgeMetadata(next) ?? {
2164
+ status: update.status,
2165
+ updated_at: update.updatedAt,
2166
+ domains: update.domains ? dedupeStrings(update.domains) : currentDomains,
2167
+ shared: update.shared ? dedupeStrings(update.shared) : currentShared,
2168
+ files: update.files ? dedupeStrings(update.files) : currentFiles
2169
+ };
2170
+ }
2171
+ function staleKnowledgeFromProgress(rawKnowledge, existing, invalidatedBy, invalidatedAt) {
2172
+ if (!existing || existing.status !== "updated" && existing.status !== "none") {
2173
+ return existing;
2174
+ }
2175
+ return mergeKnowledgeMetadata(rawKnowledge, existing, {
2176
+ status: "stale",
2177
+ updatedAt: invalidatedAt,
2178
+ invalidatedBy,
2179
+ reason: `${invalidatedBy} changed after knowledge was marked ${existing.status}.`
2180
+ });
2181
+ }
2182
+ async function resolveProgressSources(options, changePath) {
2183
+ const requested = options.sources ?? [];
2184
+ const sources = dedupeSources(requested.map((source) => normalizeArtifactSourceId(source)));
2185
+ if (sources.length > 0) {
2186
+ return { sources };
2187
+ }
2188
+ if (options.stage === "issues" && (await resolveArchitectureArtifact(changePath)).substantive) {
2189
+ return { sources: ["architecture"] };
2190
+ }
2191
+ return {
2192
+ sources,
2193
+ note: `No sources recorded for ${options.stage}; downstream stale invalidation will only use explicitly recorded dependencies.`
2194
+ };
2195
+ }
2196
+ function normalizeArtifactSourceId(value) {
2197
+ if (isArtifactSourceId(value)) {
2198
+ return value;
2199
+ }
2200
+ throw new ChangeCommandError(
2201
+ "unsupported_source",
2202
+ `Unsupported artifact source: ${value}. Expected ${artifactSourceIds.join(", ")}`,
2203
+ { source: value, supported: artifactSourceIds }
2204
+ );
2205
+ }
2206
+ function dedupeSources(sources) {
2207
+ return [...new Set(sources)];
2208
+ }
2209
+ function resolveStalePropagationTargets(input) {
2210
+ if (input.noInvalidate && input.invalidateOnly && input.invalidateOnly.length > 0) {
2211
+ throw new ChangeCommandError(
2212
+ "conflicting_stale_flags",
2213
+ "Use either --no-invalidate or --invalidate=<list>, not both."
2214
+ );
2215
+ }
2216
+ if (input.noInvalidate) {
2217
+ return [];
2218
+ }
2219
+ if (input.invalidateOnly && input.invalidateOnly.length > 0) {
2220
+ const computed = new Set(input.computed);
2221
+ const intersection = [];
2222
+ const unknown = [];
2223
+ for (const candidate of input.invalidateOnly) {
2224
+ if (computed.has(candidate)) {
2225
+ intersection.push(candidate);
2226
+ } else {
2227
+ unknown.push(candidate);
2228
+ }
2229
+ }
2230
+ if (unknown.length > 0) {
2231
+ throw new ChangeCommandError(
2232
+ "invalid_invalidate_target",
2233
+ `Cannot invalidate lanes that are not transitive dependents of the progressed lane: ${unknown.join(", ")}`,
2234
+ { unknown, computedDependents: input.computed }
2235
+ );
2236
+ }
2237
+ return intersection;
2238
+ }
2239
+ return input.computed;
2240
+ }
2241
+ function transitiveDependents(source, artifacts) {
2242
+ const dependents = /* @__PURE__ */ new Set();
2243
+ const queue = [source];
2244
+ for (let index = 0; index < queue.length; index += 1) {
2245
+ const current = queue[index];
2246
+ for (const candidate of changeStages) {
2247
+ if (candidate === source || dependents.has(candidate)) {
2248
+ continue;
2249
+ }
2250
+ const candidateSources = artifacts[candidate]?.sources ?? [];
2251
+ if (candidateSources.some((candidateSource) => candidateSource === current)) {
2252
+ dependents.add(candidate);
2253
+ queue.push(candidate);
2254
+ }
2255
+ }
2256
+ }
2257
+ return changeStages.filter((stage) => dependents.has(stage));
2258
+ }
2259
+ function compareChangesNewestFirst(left, right) {
2260
+ const leftTime = left.created_at ?? left.id;
2261
+ const rightTime = right.created_at ?? right.id;
2262
+ return rightTime.localeCompare(leftTime);
2263
+ }
2264
+ function resolveChangeReference(changes, value) {
2265
+ const ref = value.trim().toLowerCase();
2266
+ const matches = changes.filter((change) => {
2267
+ const token = change.id.split("-")[1];
2268
+ return change.id.toLowerCase() === ref || token === ref || change.slug.toLowerCase().includes(ref) || change.title.toLowerCase().includes(ref);
2269
+ });
2270
+ if (matches.length === 1) {
2271
+ return matches[0];
2272
+ }
2273
+ if (matches.length === 0) {
2274
+ throw new ChangeCommandError("change_not_found", `No matching change found: ${value}`);
2275
+ }
2276
+ throw new ChangeCommandError(
2277
+ "ambiguous_change",
2278
+ `Ambiguous change reference: ${value}
2279
+ ${matches.map((change) => ` ${change.id} - ${change.title}`).join("\n")}`,
2280
+ { candidates: matches.map((change) => ({ id: change.id, title: change.title, type: change.type, stage: change.stage })) }
2281
+ );
2282
+ }
2283
+ async function currentContextForTarget(session, target, now, options) {
2284
+ const saved = activeChangeForTarget(session, target);
2285
+ const changes = await readChanges(target.path, saved?.id);
2286
+ const branch = await currentBranch2(target.path);
2287
+ const inferred = branch ? inferChangeFromBranch(changes, branch) : void 0;
2288
+ if (saved && inferred && saved.id !== inferred.id) {
2289
+ const savedChange = changes.find((change) => change.id === saved.id);
2290
+ return {
2291
+ id: target.id,
2292
+ name: target.name,
2293
+ path: target.path,
2294
+ source: "session",
2295
+ saved: false,
2296
+ current: savedChange ? { ...savedChange, active: true } : void 0,
2297
+ branch,
2298
+ branchMatch: "mismatch",
2299
+ mismatch: { session: saved, branch: inferred }
2300
+ };
2301
+ }
2302
+ if (saved) {
2303
+ const current = changes.find((change) => change.id === saved.id);
2304
+ return {
2305
+ id: target.id,
2306
+ name: target.name,
2307
+ path: target.path,
2308
+ source: "session",
2309
+ saved: false,
2310
+ current: current ? { ...current, active: true } : void 0,
2311
+ branch,
2312
+ branchMatch: current ? branchMatch(branch, current.branch, await findGitRoot(target.path)) : "unknown"
2313
+ };
2314
+ }
2315
+ if (inferred && options.saveInferred) {
2316
+ setCurrentChangeForPath(session, target.path, { id: inferred.id, path: inferred.path, branch: inferred.branch }, now);
2317
+ return {
2318
+ id: target.id,
2319
+ name: target.name,
2320
+ path: target.path,
2321
+ source: "inferred_saved",
2322
+ saved: true,
2323
+ current: { ...inferred, active: true },
2324
+ branch,
2325
+ branchMatch: "match"
2326
+ };
2327
+ }
2328
+ return {
2329
+ id: target.id,
2330
+ name: target.name,
2331
+ path: target.path,
2332
+ source: "none",
2333
+ saved: false,
2334
+ branch,
2335
+ branchMatch: branch ? "unknown" : await findGitRoot(target.path) ? "unknown" : "not_git"
2336
+ };
2337
+ }
2338
+ function activeChangeForTarget(session, target) {
2339
+ const id = target.id ?? (session ? findFolderByPath(session, target.path) : void 0);
2340
+ return id ? session?.folders[id]?.current_change : void 0;
2341
+ }
2342
+ function activeArtifactForTarget(session, target) {
2343
+ const id = target.id ?? (session ? findFolderByPath(session, target.path) : void 0);
2344
+ return id ? session?.folders[id]?.current_artifact : void 0;
2345
+ }
2346
+ function inferChangeFromBranch(changes, branch) {
2347
+ if (!branch.startsWith("change/")) {
2348
+ return void 0;
2349
+ }
2350
+ const id = branch.slice("change/".length);
2351
+ return changes.find((change) => change.id === id);
2352
+ }
2353
+ async function saveCurrentForTargets(sessionPath, updates, now) {
2354
+ const pathToSession = sessionPath ?? defaultSessionPath();
2355
+ const session = await loadOrCreateSession(pathToSession, now);
2356
+ for (const update of updates) {
2357
+ const changeRelativePath = path8.relative(update.root, update.changePath);
2358
+ setCurrentChangeForPath(
2359
+ session,
2360
+ update.root,
2361
+ {
2362
+ id: update.changeId,
2363
+ path: changeRelativePath,
2364
+ branch: update.branch
2365
+ },
2366
+ now
2367
+ );
2368
+ if (update.artifact) {
2369
+ setCurrentArtifactForPath(
2370
+ session,
2371
+ update.root,
2372
+ {
2373
+ artifact: update.artifact,
2374
+ change_id: update.changeId,
2375
+ path: artifactPath(changeRelativePath, update.artifact)
2376
+ },
2377
+ now
2378
+ );
2379
+ }
2380
+ }
2381
+ await saveCurrentSession(session, pathToSession);
2382
+ }
2383
+ function artifactPath(changePath, artifact) {
2384
+ return path8.join(changePath, artifactFileName(artifact));
2385
+ }
2386
+ async function loadOrCreateSession(sessionPath, now) {
2387
+ return await loadCurrentSession(sessionPath) ?? {
2388
+ version: 1,
2389
+ updated_at: now.toISOString(),
2390
+ folders: {}
2391
+ };
2392
+ }
2393
+ function statusTemplate(input) {
2394
+ return YAML7.stringify({
2395
+ version: 1,
2396
+ id: input.id,
2397
+ slug: input.slug,
2398
+ title: input.title,
2399
+ type: input.type,
2400
+ stage: input.type === "feat" ? "exploration" : "started",
2401
+ branch: input.branch,
2402
+ created_at: input.now.toISOString(),
2403
+ updated_at: input.now.toISOString()
2404
+ });
2405
+ }
2406
+ function explorationTemplate(title, topic, now) {
2407
+ return `${artifactFrontmatter({ artifact: "exploration", now })}# ${titleFromSlug(slugify(title, "change")) || title}
2408
+
2409
+ ## Topic
2410
+
2411
+ ${topic}
2412
+
2413
+ ## Current Understanding
2414
+
2415
+ ## Open Questions
2416
+
2417
+ ## Decisions
2418
+
2419
+ ## Scenarios
2420
+
2421
+ ## Existing Behavior
2422
+
2423
+ ## PRD Readiness
2424
+
2425
+ Not ready
2426
+ `;
2427
+ }
2428
+ async function ensureChangeBranch(cwd, branch) {
2429
+ const gitRoot = await findGitRoot(cwd);
2430
+ if (!gitRoot) {
2431
+ return "skipped_not_git";
2432
+ }
2433
+ const current = await git2(["branch", "--show-current"], gitRoot);
2434
+ if (current === branch) {
2435
+ return "already_active";
2436
+ }
2437
+ const exists = await git2(["rev-parse", "--verify", `refs/heads/${branch}`], gitRoot);
2438
+ if (exists) {
2439
+ await gitRequired(["checkout", branch], gitRoot);
2440
+ return "checked_out";
2441
+ }
2442
+ await gitRequired(["checkout", "-b", branch], gitRoot);
2443
+ return "created";
2444
+ }
2445
+ async function assertCleanGitTargets(targets) {
2446
+ for (const target of targets) {
2447
+ const gitRoot = await findGitRoot(target.path);
2448
+ if (!gitRoot) {
2449
+ continue;
2450
+ }
2451
+ const dirty = await git2(["status", "--porcelain"], gitRoot);
2452
+ if (dirty) {
2453
+ throw new ChangeCommandError("dirty_worktree", `Uncommitted changes in ${target.path}. Commit, stash, or clean them before switching change context.`, {
2454
+ path: target.path
2455
+ });
2456
+ }
2457
+ }
2458
+ }
2459
+ async function currentBranch2(cwd) {
2460
+ const gitRoot = await findGitRoot(cwd);
2461
+ if (!gitRoot) {
2462
+ return void 0;
2463
+ }
2464
+ return git2(["branch", "--show-current"], gitRoot);
2465
+ }
2466
+ function branchMatch(branch, expected, gitRoot) {
2467
+ if (!gitRoot) {
2468
+ return "not_git";
2469
+ }
2470
+ if (!branch) {
2471
+ return "unknown";
2472
+ }
2473
+ return branch === expected ? "match" : "mismatch";
2474
+ }
2475
+ async function git2(args, cwd) {
2476
+ try {
2477
+ const { stdout } = await execFileAsync2("git", args, { cwd });
2478
+ const value = stdout.trim();
2479
+ return value.length > 0 ? value : void 0;
2480
+ } catch {
2481
+ return void 0;
2482
+ }
2483
+ }
2484
+ async function gitRequired(args, cwd) {
2485
+ await execFileAsync2("git", args, { cwd });
2486
+ }
2487
+ function summarizeChangeOperation(input) {
2488
+ const lines = [
2489
+ `${input.verb} change: ${input.id}`,
2490
+ "",
2491
+ `Title: ${input.title}`,
2492
+ `Type: ${input.type}`,
2493
+ `Branch: ${input.branch}`,
2494
+ "",
2495
+ "Targets:",
2496
+ ...input.targets.map((target) => ` ${target.path} (${formatBranchStatus(target.branchStatus)}, current)`)
2497
+ ];
2498
+ return {
2499
+ status: "ok",
2500
+ id: input.id,
2501
+ slug: input.slug,
2502
+ title: input.title,
2503
+ type: input.type,
2504
+ branch: input.branch,
2505
+ targets: input.targets,
2506
+ message: lines.join("\n")
2507
+ };
2508
+ }
2509
+ function formatListMessage(targets) {
2510
+ if (targets.length === 0) {
2511
+ return "No workspace folders found.";
2512
+ }
2513
+ return targets.map((target) => {
2514
+ const lines = [`Changes in ${target.name ?? target.path}`];
2515
+ if (target.changes.length === 0) {
2516
+ lines.push(" No changes found.");
2517
+ } else {
2518
+ lines.push(
2519
+ ...target.changes.map((change) => `${change.active ? "*" : " "} ${change.id} ${change.type} ${change.stage} ${change.title}`)
2520
+ );
2521
+ }
2522
+ return lines.join("\n");
2523
+ }).join("\n\n");
2524
+ }
2525
+ function formatCurrentMessage(targets) {
2526
+ if (targets.length === 0) {
2527
+ return "No workspace folders found.";
2528
+ }
2529
+ return targets.map(formatCurrentTarget).join("\n\n");
2530
+ }
2531
+ function formatCurrentTarget(target) {
2532
+ const heading = target.name ? `${target.name} (${target.path})` : target.path;
2533
+ if (!target.current) {
2534
+ if (target.mismatch) {
2535
+ return [
2536
+ `Current change in ${heading}: mismatch`,
2537
+ `Session: ${target.mismatch.session.id}`,
2538
+ `Branch: ${target.mismatch.branch.id}`,
2539
+ `Resolve with: weave change switch ${target.mismatch.branch.id}`
2540
+ ].join("\n");
2541
+ }
2542
+ return `Current change in ${heading}: none`;
2543
+ }
2544
+ return [
2545
+ `Current change in ${heading}: ${target.current.id}`,
2546
+ `Title: ${target.current.title}`,
2547
+ `Type: ${target.current.type}`,
2548
+ `Stage: ${target.current.stage}`,
2549
+ ...formatStaleLines(target.current.stale),
2550
+ ...formatKnowledgeLines(target.current.knowledge),
2551
+ `Branch: ${target.current.branch}`,
2552
+ `Path: ${target.current.path}`,
2553
+ `Source: ${formatCurrentSource(target.source)}`
2554
+ ].join("\n");
2555
+ }
2556
+ function formatStatusMessage(targets) {
2557
+ if (targets.length === 0) {
2558
+ return "No workspace folders found.";
2559
+ }
2560
+ return targets.map(formatStatusTarget).join("\n\n");
2561
+ }
2562
+ function formatStatusTarget(target) {
2563
+ const heading = target.name ? `${target.name} (${target.path})` : target.path;
2564
+ if (target.mismatch) {
2565
+ return [
2566
+ `Status in ${heading}: mismatch`,
2567
+ `Session: ${target.mismatch.session.id}`,
2568
+ `Branch: ${target.mismatch.branch.id}`,
2569
+ `Resolve with: weave change switch ${target.mismatch.branch.id}`
2570
+ ].join("\n");
2571
+ }
2572
+ if (!target.change) {
2573
+ return `Status in ${heading}: no active change`;
2574
+ }
2575
+ return [
2576
+ `Status in ${heading}: ${target.change.id}`,
2577
+ `Title: ${target.change.title}`,
2578
+ `Type: ${target.change.type}`,
2579
+ `Stage: ${target.change.stage}`,
2580
+ ...formatStaleLines(target.change.stale),
2581
+ ...formatKnowledgeLines(target.change.knowledge),
2582
+ `Branch: ${target.change.branch}`,
2583
+ `Path: ${target.change.path}`,
2584
+ `Active: ${target.active ? "yes" : "no"}`,
2585
+ `Branch match: ${target.branchMatch}`,
2586
+ `Source: ${formatCurrentSource(target.source)}`
2587
+ ].join("\n");
2588
+ }
2589
+ function formatProgressMessage(target, change, progressed, note) {
2590
+ const heading = target.name ? `${target.name} (${target.path})` : target.path;
2591
+ return [
2592
+ `Progressed change in ${heading}: ${change.id}`,
2593
+ `Progressed lane: ${progressed}`,
2594
+ `Stage: ${change.stage}`,
2595
+ ...formatStaleLines(change.stale),
2596
+ ...formatKnowledgeLines(change.knowledge),
2597
+ ...note ? [`Note: ${note}`] : [],
2598
+ `Path: ${change.path}`
2599
+ ].join("\n");
2600
+ }
2601
+ function formatKnowledgeMessage(target, change) {
2602
+ const heading = target.name ? `${target.name} (${target.path})` : target.path;
2603
+ return [
2604
+ `Updated knowledge status in ${heading}: ${change.id}`,
2605
+ ...formatKnowledgeLines(change.knowledge),
2606
+ `Stage: ${change.stage}`,
2607
+ `Path: ${change.path}`
2608
+ ].join("\n");
2609
+ }
2610
+ function formatStaleLines(stale) {
2611
+ const entries = changeStages.filter((stage) => stale[stage]).map((stage) => {
2612
+ const metadata = stale[stage];
2613
+ return `${stage} (invalidated by ${metadata.invalidated_by})`;
2614
+ });
2615
+ return entries.length > 0 ? [`Stale: ${entries.join(", ")}`] : [];
2616
+ }
2617
+ function formatKnowledgeLines(knowledge) {
2618
+ if (!knowledge) {
2619
+ return [];
2620
+ }
2621
+ const suffix = knowledge.status === "stale" && knowledge.invalidated_by ? ` (invalidated by ${knowledge.invalidated_by})` : "";
2622
+ return [`Knowledge: ${knowledge.status}${suffix}`];
2623
+ }
2624
+ function formatCurrentSource(source) {
2625
+ switch (source) {
2626
+ case "inferred_saved":
2627
+ return "inferred from branch and saved";
2628
+ case "session":
2629
+ return "session";
2630
+ case "explicit":
2631
+ return "explicit";
2632
+ case "none":
2633
+ return "none";
2634
+ }
2635
+ }
2636
+ function formatBranchStatus(status) {
2637
+ switch (status) {
2638
+ case "already_active":
2639
+ return "branch already active";
2640
+ case "checked_out":
2641
+ return "branch checked out";
2642
+ case "created":
2643
+ return "branch created";
2644
+ case "skipped_not_git":
2645
+ return "branch skipped: not a git repo";
2646
+ }
2647
+ }
2648
+ function isChangeType(value) {
2649
+ return typeof value === "string" && changeTypes.includes(value);
2650
+ }
2651
+ function isChangeStage(value) {
2652
+ return typeof value === "string" && changeStages.includes(value);
2653
+ }
2654
+ function isStoredChangeStage(value) {
2655
+ return typeof value === "string" && storedStages.includes(value);
2656
+ }
2657
+ function isArtifactSourceId(value) {
2658
+ return typeof value === "string" && artifactSourceIds.includes(value);
2659
+ }
2660
+ function isKnowledgeStatus(value) {
2661
+ return typeof value === "string" && knowledgeStatuses.includes(value);
2662
+ }
2663
+ function isKnowledgeInvalidationSource(value) {
2664
+ return isChangeStage(value) || isArtifactSourceId(value);
2665
+ }
2666
+ function normalizeKnowledgeInvalidationSource(value) {
2667
+ if (isKnowledgeInvalidationSource(value)) {
2668
+ return value;
2669
+ }
2670
+ const supported = dedupeStrings([...changeStages, ...artifactSourceIds]);
2671
+ throw new ChangeCommandError(
2672
+ "unsupported_knowledge_invalidation_source",
2673
+ `Unsupported knowledge invalidation source: ${value}. Expected ${supported.join(", ")}`,
2674
+ { source: value, supported }
2675
+ );
2676
+ }
2677
+ function maxStage(stages) {
2678
+ const highest = stages.reduce(
2679
+ (acc, stage) => stageIndex(stage) > stageIndex(acc) ? stage : acc,
2680
+ "exploration"
2681
+ );
2682
+ return isChangeStage(highest) ? highest : "exploration";
2683
+ }
2684
+ function stageIndex(stage) {
2685
+ return changeStages.indexOf(stage);
2686
+ }
2687
+ function isRecord(value) {
2688
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2689
+ }
2690
+ function isNodeError2(error) {
2691
+ return error instanceof Error && "code" in error;
2692
+ }
2693
+
2694
+ // src/lib/artifact-context.ts
2695
+ async function currentArtifact(options) {
2696
+ const now = options.now ?? /* @__PURE__ */ new Date();
2697
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
2698
+ const changeResult = await currentChange({ cwd: options.cwd, now, sessionPath });
2699
+ const session = await loadCurrentSession(sessionPath);
2700
+ const targets = changeResult.targets.map((target) => {
2701
+ const saved = currentArtifactForPath(session, target.path);
2702
+ const valid = Boolean(saved && target.current && saved.change_id === target.current.id);
2703
+ return {
2704
+ id: target.id,
2705
+ name: target.name,
2706
+ path: target.path,
2707
+ source: valid ? "session" : "none",
2708
+ current: valid,
2709
+ artifact: valid ? saved : void 0,
2710
+ current_change: target.current ? { id: target.current.id, path: target.current.path, branch: target.current.branch } : void 0
2711
+ };
2712
+ });
2713
+ return {
2714
+ status: "ok",
2715
+ targets,
2716
+ message: formatCurrentArtifactMessage(targets)
2717
+ };
2718
+ }
2719
+ async function setCurrentArtifact(options) {
2720
+ const artifact = parseArtifact(options.artifact);
2721
+ const now = options.now ?? /* @__PURE__ */ new Date();
2722
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
2723
+ const changeResult = await currentChange({ cwd: options.cwd, now, sessionPath });
2724
+ if (changeResult.targets.length !== 1) {
2725
+ throw new ChangeCommandError("ambiguous_target", "Set current artifact for one target at a time");
2726
+ }
2727
+ const target = changeResult.targets[0];
2728
+ if (!target.current) {
2729
+ throw new ChangeCommandError("no_current_change", "No active Weave change found. Run `weave change new` or `weave change switch` first.");
2730
+ }
2731
+ const session = await loadCurrentSession(sessionPath) ?? {
2732
+ version: 1,
2733
+ updated_at: now.toISOString(),
2734
+ folders: {}
2735
+ };
2736
+ const artifactState = {
2737
+ artifact,
2738
+ change_id: target.current.id,
2739
+ path: await resolveArtifactContextPath(target.current.path, target.current.changePath, artifact)
2740
+ };
2741
+ setCurrentArtifactForPath(session, target.path, artifactState, now);
2742
+ await saveCurrentSession(session, sessionPath);
2743
+ const saved = currentArtifactForPath(session, target.path);
2744
+ const results = [
2745
+ {
2746
+ id: target.id,
2747
+ name: target.name,
2748
+ path: target.path,
2749
+ source: "session",
2750
+ current: true,
2751
+ artifact: saved,
2752
+ current_change: {
2753
+ id: target.current.id,
2754
+ path: target.current.path,
2755
+ branch: target.current.branch
2756
+ }
2757
+ }
2758
+ ];
2759
+ return {
2760
+ status: "ok",
2761
+ targets: results,
2762
+ message: formatCurrentArtifactMessage(results)
2763
+ };
2764
+ }
2765
+ async function clearCurrentArtifact(options) {
2766
+ const now = options.now ?? /* @__PURE__ */ new Date();
2767
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
2768
+ const changeResult = await currentChange({ cwd: options.cwd, now, sessionPath });
2769
+ if (changeResult.targets.length !== 1) {
2770
+ throw new ChangeCommandError("ambiguous_target", "Clear current artifact for one target at a time");
2771
+ }
2772
+ const target = changeResult.targets[0];
2773
+ const session = await loadCurrentSession(sessionPath) ?? {
2774
+ version: 1,
2775
+ updated_at: now.toISOString(),
2776
+ folders: {}
2777
+ };
2778
+ clearCurrentArtifactForPath(session, target.path, now);
2779
+ await saveCurrentSession(session, sessionPath);
2780
+ const results = [
2781
+ {
2782
+ id: target.id,
2783
+ name: target.name,
2784
+ path: target.path,
2785
+ source: "none",
2786
+ current: false,
2787
+ current_change: target.current ? { id: target.current.id, path: target.current.path, branch: target.current.branch } : void 0
2788
+ }
2789
+ ];
2790
+ return {
2791
+ status: "ok",
2792
+ targets: results,
2793
+ message: formatCurrentArtifactMessage(results)
2794
+ };
2795
+ }
2796
+ function parseArtifact(value) {
2797
+ if (isArtifactName(value)) {
2798
+ return value;
2799
+ }
2800
+ throw new ChangeCommandError("invalid_artifact", `Unsupported artifact: ${value}. Expected exploration, prd, or architecture.`);
2801
+ }
2802
+ async function resolveArtifactContextPath(changeRelativePath, changeAbsolutePath, artifact) {
2803
+ if (artifact !== "architecture") {
2804
+ return path9.join(changeRelativePath, artifactFileName(artifact));
2805
+ }
2806
+ const architecture = await resolveArchitectureArtifact(changeAbsolutePath);
2807
+ if ((architecture.status === "folder" || architecture.status === "conflict") && architecture.indexExists) {
2808
+ return path9.join(changeRelativePath, "architecture", "index.md");
2809
+ }
2810
+ return path9.join(changeRelativePath, "architecture.md");
2811
+ }
2812
+ function formatCurrentArtifactMessage(targets) {
2813
+ return targets.map((target) => {
2814
+ const label = target.name ? `${target.name} (${target.path})` : target.path;
2815
+ if (!target.artifact) {
2816
+ return [`Target: ${label}`, "Current artifact: none"].join("\n");
2817
+ }
2818
+ return [
2819
+ `Target: ${label}`,
2820
+ `Current artifact: ${target.artifact.artifact}`,
2821
+ `Change: ${target.artifact.change_id}`,
2822
+ `Path: ${target.artifact.path}`
2823
+ ].join("\n");
2824
+ }).join("\n\n");
2825
+ }
2826
+
2827
+ // src/commands/artifact.ts
2828
+ function artifactCommand() {
2829
+ const command = new Command3("artifact").description("Inspect and route Weave artifact context.");
2830
+ const current = new Command3("current").description("Show, set, or clear the current artifact context.").option("--json", "print machine-readable JSON").action(async (options) => {
2831
+ await runAction2(options.json ?? false, async () => {
2832
+ const result3 = await currentArtifact({
2833
+ cwd: process.cwd()
2834
+ });
2835
+ writeResult(result3, options.json ?? false);
2836
+ });
2837
+ });
2838
+ current.command("set").description("Set the current artifact context for the active change.").argument("<artifact>", "artifact: exploration, prd, or architecture", parseArtifactName).option("--json", "print machine-readable JSON").action(async (artifact, options) => {
2839
+ await runAction2(options.json ?? false, async () => {
2840
+ const result3 = await setCurrentArtifact({
2841
+ cwd: process.cwd(),
2842
+ artifact
2843
+ });
2844
+ writeResult(result3, options.json ?? false);
2845
+ });
2846
+ });
2847
+ current.command("clear").description("Clear the current artifact context for the active change.").option("--json", "print machine-readable JSON").action(async (options) => {
2848
+ await runAction2(options.json ?? false, async () => {
2849
+ const result3 = await clearCurrentArtifact({
2850
+ cwd: process.cwd()
2851
+ });
2852
+ writeResult(result3, options.json ?? false);
2853
+ });
2854
+ });
2855
+ command.addCommand(current);
2856
+ return command;
2857
+ }
2858
+ function parseArtifactName(value) {
2859
+ if (isArtifactName(value)) {
2860
+ return value;
2861
+ }
2862
+ throw new InvalidArgumentError2(`Unsupported artifact: ${value}`);
2863
+ }
2864
+ function writeResult(result3, json) {
2865
+ if (json) {
2866
+ process.stdout.write(`${JSON.stringify(result3, null, 2)}
2867
+ `);
2868
+ return;
2869
+ }
2870
+ process.stdout.write(`${result3.message}
2871
+ `);
2872
+ }
2873
+ async function runAction2(json, action) {
2874
+ try {
2875
+ await action();
2876
+ } catch (error) {
2877
+ if (json) {
2878
+ process.stdout.write(`${JSON.stringify(errorResult(error), null, 2)}
2879
+ `);
2880
+ } else {
2881
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
2882
+ `);
2883
+ }
2884
+ process.exitCode = 1;
2885
+ }
2886
+ }
2887
+ function errorResult(error) {
2888
+ if (error instanceof ChangeCommandError) {
2889
+ return {
2890
+ status: "error",
2891
+ code: error.code,
2892
+ message: error.message,
2893
+ details: error.details
2894
+ };
2895
+ }
2896
+ return {
2897
+ status: "error",
2898
+ code: "unknown_error",
2899
+ message: error instanceof Error ? error.message : String(error)
2900
+ };
2901
+ }
2902
+
2903
+ // src/commands/change.ts
2904
+ import { Command as Command4, InvalidArgumentError as InvalidArgumentError3 } from "commander";
2905
+
2906
+ // src/lib/with-notices.ts
2907
+ import { dirname as dirname3, join as join4 } from "path";
2908
+ import { fileURLToPath as fileURLToPath2 } from "url";
2909
+ import { readFile as readFile9 } from "fs/promises";
2910
+
2911
+ // src/lib/notices.ts
2912
+ import { createHash as createHash3 } from "crypto";
2913
+ import { readFile as readFile8 } from "fs/promises";
2914
+ import { join as join2 } from "path";
2915
+ async function gatherNotices(options) {
2916
+ const env = options.env ?? process.env;
2917
+ if (env.WEAVE_NO_NOTICES === "1") {
2918
+ return [];
2919
+ }
2920
+ const notices = [];
2921
+ if (options.npmLatest && isNewerVersion(options.npmLatest, options.packageVersion)) {
2922
+ notices.push({
2923
+ kind: "package_outdated",
2924
+ message: `weave-it ${options.npmLatest} is available (installed ${options.packageVersion}). Run 'weave status' for details.`,
2925
+ details: {
2926
+ installed: options.packageVersion,
2927
+ latest: options.npmLatest
2928
+ }
2929
+ });
2930
+ }
2931
+ const drift = await detectSkillDrift({
2932
+ cwd: options.cwd,
2933
+ templatesDir: options.templatesDir
2934
+ });
2935
+ if (drift.modified.length > 0) {
2936
+ notices.push({
2937
+ kind: "skills_modified",
2938
+ message: `${drift.modified.length} installed skill file(s) have been modified locally. Run 'weave status' for details.`,
2939
+ details: { skills: drift.modified }
2940
+ });
2941
+ }
2942
+ if (drift.outdated.length > 0) {
2943
+ notices.push({
2944
+ kind: "skills_outdated",
2945
+ message: `${drift.outdated.length} installed skill file(s) are out of date. Run 'weave agent update --all' or 'weave status' for details.`,
2946
+ details: { skills: drift.outdated }
2947
+ });
2948
+ }
2949
+ return notices;
2950
+ }
2951
+ async function detectSkillDrift(options) {
2952
+ const manifest = await loadAgentsManifest(options.cwd);
2953
+ const templates = await safeListDefaultSkills(options.templatesDir);
2954
+ const templatesByName = new Map(templates.map((skill) => [skill.name, skill]));
2955
+ const modified = [];
2956
+ const outdated = [];
2957
+ for (const [agentKey, buckets] of Object.entries(manifest.installed)) {
2958
+ if (!buckets) continue;
2959
+ const skillEntries = buckets.skills ?? {};
2960
+ for (const [name, entry] of Object.entries(skillEntries)) {
2961
+ const absolutePath = join2(options.cwd, entry.path);
2962
+ const diskHash = await safeHashFile(absolutePath);
2963
+ if (diskHash === null) {
2964
+ continue;
2965
+ }
2966
+ if (diskHash !== entry.installed_hash) {
2967
+ modified.push({ agent: agentKey, name, kind: "skill" });
2968
+ continue;
2969
+ }
2970
+ const template = templatesByName.get(name);
2971
+ if (!template) continue;
2972
+ if (entry.installed_from !== template.lastChangedIn) {
2973
+ outdated.push({
2974
+ agent: agentKey,
2975
+ name,
2976
+ kind: "skill",
2977
+ installed_from: entry.installed_from,
2978
+ current: template.lastChangedIn
2979
+ });
2980
+ }
2981
+ }
2982
+ }
2983
+ modified.sort(compareSkillRefs);
2984
+ outdated.sort(compareSkillRefs);
2985
+ return { modified, outdated };
2986
+ }
2987
+ async function safeListDefaultSkills(templatesDir) {
2988
+ try {
2989
+ return await listDefaultSkills({ templatesDir });
2990
+ } catch {
2991
+ return [];
2992
+ }
2993
+ }
2994
+ async function safeHashFile(path14) {
2995
+ try {
2996
+ const content = await readFile8(path14, "utf8");
2997
+ return `sha256:${createHash3("sha256").update(content).digest("hex")}`;
2998
+ } catch {
2999
+ return null;
3000
+ }
3001
+ }
3002
+ function compareSkillRefs(left, right) {
3003
+ const agentCompare = left.agent.localeCompare(right.agent);
3004
+ if (agentCompare !== 0) return agentCompare;
3005
+ return left.name.localeCompare(right.name);
3006
+ }
3007
+ function isNewerVersion(candidate, baseline) {
3008
+ const parsedCandidate = parseSemver(candidate);
3009
+ const parsedBaseline = parseSemver(baseline);
3010
+ if (!parsedCandidate || !parsedBaseline) {
3011
+ return false;
3012
+ }
3013
+ for (let index = 0; index < 3; index++) {
3014
+ if (parsedCandidate[index] > parsedBaseline[index]) return true;
3015
+ if (parsedCandidate[index] < parsedBaseline[index]) return false;
3016
+ }
3017
+ return false;
3018
+ }
3019
+ function parseSemver(value) {
3020
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(value);
3021
+ if (!match) return null;
3022
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
3023
+ }
3024
+
3025
+ // src/lib/user-paths.ts
3026
+ import { homedir } from "os";
3027
+ import { join as join3 } from "path";
3028
+ function getUserHome(env = process.env) {
3029
+ return env.WEAVE_USER_HOME || homedir();
3030
+ }
3031
+ function getUserWeaveDir(env = process.env) {
3032
+ return join3(getUserHome(env), ".weave");
3033
+ }
3034
+ function getUserCacheDir(env = process.env) {
3035
+ return join3(getUserWeaveDir(env), "cache");
3036
+ }
3037
+ function getNpmVersionCachePath(env = process.env) {
3038
+ return join3(getUserCacheDir(env), "npm-version.json");
3039
+ }
3040
+
3041
+ // src/lib/npm-version.ts
3042
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/weave-it/latest";
3043
+ var FETCH_TIMEOUT_MS = 3e3;
3044
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
3045
+ async function getNpmVersionInfo(options) {
3046
+ const env = options.env ?? process.env;
3047
+ if (env.NO_UPDATE_NOTIFIER === "1" || env.WEAVE_NO_NOTICES === "1") {
3048
+ return { cachedLatest: null, isStale: true, fetchedAt: null };
3049
+ }
3050
+ const now = options.now ?? /* @__PURE__ */ new Date();
3051
+ const cachePath = options.cachePath ?? getNpmVersionCachePath(env);
3052
+ const cache = await readJsonCache(cachePath);
3053
+ let cachedLatest = null;
3054
+ let fetchedAt = null;
3055
+ let isStale = true;
3056
+ if (cache && typeof cache.latest === "string" && typeof cache.fetched_at === "string") {
3057
+ const parsed = Date.parse(cache.fetched_at);
3058
+ if (!Number.isNaN(parsed)) {
3059
+ cachedLatest = cache.latest;
3060
+ fetchedAt = new Date(parsed);
3061
+ isStale = now.getTime() - parsed > CACHE_TTL_MS;
3062
+ }
3063
+ }
3064
+ if (isStale) {
3065
+ void refreshNpmVersionCache({
3066
+ cachePath,
3067
+ now,
3068
+ fetchImpl: options.fetchImpl ?? fetch,
3069
+ packageVersion: options.packageVersion
3070
+ });
3071
+ }
3072
+ return { cachedLatest, isStale, fetchedAt };
3073
+ }
3074
+ async function refreshNpmVersionCache(opts) {
3075
+ const controller = new AbortController();
3076
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
3077
+ try {
3078
+ const response = await opts.fetchImpl(NPM_REGISTRY_URL, {
3079
+ signal: controller.signal,
3080
+ headers: { "user-agent": `weave-it/${opts.packageVersion}` }
3081
+ });
3082
+ if (!response.ok) return;
3083
+ const parsed = await response.json();
3084
+ if (typeof parsed?.version !== "string") return;
3085
+ await writeJsonCache(opts.cachePath, {
3086
+ latest: parsed.version,
3087
+ fetched_at: opts.now.toISOString()
3088
+ });
3089
+ } catch {
3090
+ } finally {
3091
+ clearTimeout(timeout);
3092
+ }
3093
+ }
3094
+
3095
+ // src/lib/with-notices.ts
3096
+ async function withNotices(options, action) {
3097
+ const cwd = options.cwd ?? process.cwd();
3098
+ const env = options.env ?? process.env;
3099
+ const packageVersion = options.packageVersion ?? await readPackageVersion();
3100
+ const noticesPromise = gatherNoticesSafely({ cwd, packageVersion, env });
3101
+ const actionResult = await action();
3102
+ const notices = await noticesPromise;
3103
+ if (options.json) {
3104
+ const payload = mergeNoticesIntoJson(actionResult.json, notices);
3105
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
3106
+ `);
3107
+ } else {
3108
+ process.stdout.write(`${actionResult.text}
3109
+ `);
3110
+ if (shouldShowFooter(notices, env)) {
3111
+ process.stderr.write(renderFooter(notices));
3112
+ }
3113
+ }
3114
+ if (actionResult.exitCode !== void 0 && actionResult.exitCode !== 0) {
3115
+ process.exitCode = actionResult.exitCode;
3116
+ }
3117
+ }
3118
+ async function gatherNoticesSafely(opts) {
3119
+ try {
3120
+ const npmInfo = await getNpmVersionInfo({
3121
+ packageVersion: opts.packageVersion,
3122
+ env: opts.env
3123
+ });
3124
+ return await gatherNotices({
3125
+ cwd: opts.cwd,
3126
+ packageVersion: opts.packageVersion,
3127
+ npmLatest: npmInfo.cachedLatest,
3128
+ env: opts.env
3129
+ });
3130
+ } catch {
3131
+ return [];
3132
+ }
3133
+ }
3134
+ function mergeNoticesIntoJson(json, notices) {
3135
+ if (json && typeof json === "object" && !Array.isArray(json)) {
3136
+ return { ...json, notices };
3137
+ }
3138
+ return { result: json, notices };
3139
+ }
3140
+ function shouldShowFooter(notices, env) {
3141
+ if (notices.length === 0) return false;
3142
+ if (env.WEAVE_NO_NOTICES === "1") return false;
3143
+ if (env.CI && env.CI !== "" && env.CI !== "false" && env.CI !== "0") return false;
3144
+ if (!process.stdout.isTTY) return false;
3145
+ return true;
3146
+ }
3147
+ function renderFooter(notices) {
3148
+ return `weave-it: ${notices.length} notice(s) - run 'weave status' for details
3149
+ `;
3150
+ }
3151
+ async function readPackageVersion() {
3152
+ let current = dirname3(fileURLToPath2(import.meta.url));
3153
+ while (true) {
3154
+ const candidate = join4(current, "package.json");
3155
+ if (await pathExists(candidate)) {
3156
+ try {
3157
+ const parsed = JSON.parse(await readFile9(candidate, "utf8"));
3158
+ if (typeof parsed?.version === "string") return parsed.version;
3159
+ } catch {
3160
+ }
3161
+ }
3162
+ const parent = dirname3(current);
3163
+ if (parent === current) return "0.0.0";
3164
+ current = parent;
3165
+ }
3166
+ }
3167
+
3168
+ // src/commands/change.ts
3169
+ function changeCommand() {
3170
+ const command = new Command4("change").description("Create and inspect Weave change artifacts.");
3171
+ command.command("new").description("Create a change exploration.").argument("<title>", "change title").option("--type <type>", "change type: feat, fix, refactor, docs, test, ci, or chore", parseChangeType, "feat").option("--slug <slug>", "change slug override").option("--json", "print machine-readable JSON").action(async (title, options) => {
3172
+ const json = options.json ?? false;
3173
+ await runAction3(json, async () => {
3174
+ await withNotices({ commandName: "change-new", json }, async () => {
3175
+ const result3 = await createChange({
3176
+ cwd: process.cwd(),
3177
+ title,
3178
+ type: options.type,
3179
+ slug: options.slug
3180
+ });
3181
+ return { json: result3, text: result3.message };
3182
+ });
3183
+ });
3184
+ });
3185
+ command.command("list").description("List changes.").option("--json", "print machine-readable JSON").action(async (options) => {
3186
+ await runAction3(options.json ?? false, async () => {
3187
+ const result3 = await listChanges({
3188
+ cwd: process.cwd()
3189
+ });
3190
+ writeResult2(result3, options.json ?? false);
3191
+ });
3192
+ });
3193
+ command.command("current").description("Show the current active change.").option("--json", "print machine-readable JSON").action(async (options) => {
3194
+ const json = options.json ?? false;
3195
+ await runAction3(json, async () => {
3196
+ await withNotices({ commandName: "change-current", json }, async () => {
3197
+ const result3 = await currentChange({ cwd: process.cwd() });
3198
+ return { json: result3, text: result3.message };
3199
+ });
3200
+ });
3201
+ });
3202
+ command.command("status").description("Show active change status and branch alignment.").argument("[change]", "change reference to inspect without switching").option("--json", "print machine-readable JSON").action(async (change, options) => {
3203
+ const json = options.json ?? false;
3204
+ await runAction3(json, async () => {
3205
+ await withNotices({ commandName: "change-status", json }, async () => {
3206
+ const result3 = await statusChange({
3207
+ cwd: process.cwd(),
3208
+ change
3209
+ });
3210
+ return { json: result3, text: result3.message };
3211
+ });
3212
+ });
3213
+ });
3214
+ command.command("progress").description("Record lifecycle progress for the active change.").argument("<lane>", "lane: exploration, prd, architecture, or issues", parseChangeStage).option("--source <source>", "source dependency: exploration, prd, architecture, discussion, sessions, or codebase", collectValues, []).option("--no-invalidate", "suppress all downstream stale propagation").option(
3215
+ "--invalidate <lanes>",
3216
+ "mark only this comma-separated subset of dependent lanes stale (e.g. issues,prd)"
3217
+ ).option("--json", "print machine-readable JSON").action(async (stage, options) => {
3218
+ await runAction3(options.json ?? false, async () => {
3219
+ const invalidate = options.invalidate;
3220
+ const result3 = await progressChange({
3221
+ cwd: process.cwd(),
3222
+ stage,
3223
+ sources: options.source,
3224
+ noInvalidate: invalidate === false,
3225
+ invalidateOnly: typeof invalidate === "string" ? parseInvalidateList(invalidate) : void 0
3226
+ });
3227
+ writeResult2(result3, options.json ?? false);
3228
+ });
3229
+ });
3230
+ command.command("clear-stale").description("Explicitly clear a stale lane flag after content-sync verification.").argument("<lane>", "lane: exploration, prd, architecture, or issues", parseChangeStage).option("--reason <reason>", "one-sentence verification rationale recorded in stale_history").option("--json", "print machine-readable JSON").action(async (lane, options) => {
3231
+ await runAction3(options.json ?? false, async () => {
3232
+ const result3 = await clearChangeStaleness({
3233
+ cwd: process.cwd(),
3234
+ lane,
3235
+ reason: options.reason
3236
+ });
3237
+ writeResult2(result3, options.json ?? false);
3238
+ });
3239
+ });
3240
+ command.command("knowledge").description("Record knowledge freshness for the active change.").argument("<status>", "knowledge status: pending, stale, updated, or none", parseKnowledgeStatus).option("--domain <domain>", "affected knowledge domain", collectValues).option("--shared <shared>", "affected shared behavior area", collectValues).option("--file <file>", "touched or authoritative knowledge file", collectValues).option("--delta <delta>", "change-local knowledge delta file").option("--reason <reason>", "reason for the knowledge status").option("--invalidated-by <source>", "source that invalidated knowledge freshness").option("--json", "print machine-readable JSON").action(async (status, options) => {
3241
+ await runAction3(options.json ?? false, async () => {
3242
+ const result3 = await knowledgeChange({
3243
+ cwd: process.cwd(),
3244
+ status,
3245
+ domains: options.domain,
3246
+ shared: options.shared,
3247
+ files: options.file,
3248
+ delta: options.delta,
3249
+ reason: options.reason,
3250
+ invalidatedBy: options.invalidatedBy
3251
+ });
3252
+ writeResult2(result3, options.json ?? false);
3253
+ });
3254
+ });
3255
+ command.command("switch").description("Switch to an existing change.").argument("<change>", "change id, token, slug, or title substring").option("--json", "print machine-readable JSON").action(async (change, options) => {
3256
+ await runAction3(options.json ?? false, async () => {
3257
+ const result3 = await switchChange({
3258
+ cwd: process.cwd(),
3259
+ change
3260
+ });
3261
+ writeResult2(result3, options.json ?? false);
3262
+ });
3263
+ });
3264
+ return command;
3265
+ }
3266
+ function parseKnowledgeStatus(value) {
3267
+ if (isKnowledgeStatus(value)) {
3268
+ return value;
3269
+ }
3270
+ throw new InvalidArgumentError3(`Unsupported knowledge status: ${value}. Expected ${knowledgeStatuses.join(", ")}`);
3271
+ }
3272
+ function parseChangeStage(value) {
3273
+ if (isChangeStage(value)) {
3274
+ return value;
3275
+ }
3276
+ throw new InvalidArgumentError3(`Unsupported change stage: ${value}. Expected ${changeStages.join(", ")}`);
3277
+ }
3278
+ function parseInvalidateList(raw) {
3279
+ if (raw === void 0) return void 0;
3280
+ const trimmed = raw.trim();
3281
+ if (trimmed.length === 0) return [];
3282
+ const parts = trimmed.split(",").map((part) => part.trim()).filter(Boolean);
3283
+ for (const part of parts) {
3284
+ if (!isChangeStage(part)) {
3285
+ throw new InvalidArgumentError3(
3286
+ `Unsupported lane in --invalidate: ${part}. Expected ${changeStages.join(", ")}`
3287
+ );
3288
+ }
3289
+ }
3290
+ return parts;
3291
+ }
3292
+ function parseChangeType(value) {
3293
+ if (changeTypes.includes(value)) {
3294
+ return value;
3295
+ }
3296
+ throw new InvalidArgumentError3(`Unsupported change type: ${value}`);
3297
+ }
3298
+ function collectValues(value, previous = []) {
3299
+ return [...previous, value];
3300
+ }
3301
+ function writeResult2(result3, json) {
3302
+ if (json) {
3303
+ process.stdout.write(`${JSON.stringify(result3, null, 2)}
3304
+ `);
3305
+ return;
3306
+ }
3307
+ process.stdout.write(`${result3.message}
3308
+ `);
3309
+ }
3310
+ async function runAction3(json, action) {
3311
+ try {
3312
+ await action();
3313
+ } catch (error) {
3314
+ if (json) {
3315
+ process.stdout.write(`${JSON.stringify(errorResult2(error), null, 2)}
3316
+ `);
3317
+ } else {
3318
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
3319
+ `);
3320
+ }
3321
+ process.exitCode = 1;
3322
+ }
3323
+ }
3324
+ function errorResult2(error) {
3325
+ if (error instanceof ChangeCommandError) {
3326
+ return {
3327
+ status: "error",
3328
+ code: error.code,
3329
+ message: error.message,
3330
+ details: error.details
3331
+ };
3332
+ }
3333
+ return {
3334
+ status: "error",
3335
+ code: "unknown_error",
3336
+ message: error instanceof Error ? error.message : String(error)
3337
+ };
3338
+ }
3339
+
3340
+ // src/commands/init.ts
3341
+ import { Command as Command5 } from "commander";
3342
+
3343
+ // src/lib/init-workspace.ts
3344
+ import * as prompts from "@clack/prompts";
3345
+ import { readFile as readFile10, realpath as realpath5 } from "fs/promises";
3346
+ import path10 from "path";
3347
+ import YAML8 from "yaml";
3348
+ async function initWorkspace(options = {}) {
3349
+ const cwd = options.cwd ?? process.cwd();
3350
+ const now = options.now ?? /* @__PURE__ */ new Date();
3351
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
3352
+ const existingSession = await loadCurrentSession(sessionPath);
3353
+ if (existingSession && !await shouldReplaceSession(options)) {
3354
+ return {
3355
+ status: "cancelled",
3356
+ message: "Cancelled. Existing Weave session was not replaced.",
3357
+ folderPath: cwd,
3358
+ wikiDir: "",
3359
+ metadataDir: "",
3360
+ sessionPath
3361
+ };
3362
+ }
3363
+ const mode = await selectInitMode(options);
3364
+ if (mode === "cancelled") {
3365
+ return {
3366
+ status: "cancelled",
3367
+ message: "Cancelled. Weave was not initialized.",
3368
+ folderPath: cwd,
3369
+ wikiDir: "",
3370
+ metadataDir: "",
3371
+ sessionPath
3372
+ };
3373
+ }
3374
+ if (mode === "workspace") {
3375
+ return initWorkspaceMode({ ...options, cwd, now, sessionPath });
3376
+ }
3377
+ return initRepoMode({ ...options, cwd, now, sessionPath });
3378
+ }
3379
+ async function initRepoMode(options) {
3380
+ const folder = await resolveFolder({
3381
+ cwd: options.cwd,
3382
+ id: options.folderId,
3383
+ kind: options.folderKind
3384
+ });
3385
+ const scaffold = await ensureWeaveScaffold({ folder });
3386
+ const workspaceMetadata = await writeRepoWorkspaceMetadata(folder);
3387
+ if (workspaceMetadata) {
3388
+ scaffold.created.push(".weave/workspace.yml");
3389
+ }
3390
+ const session = createCurrentSession(folder, options.now);
3391
+ await saveCurrentSession(session, options.sessionPath);
3392
+ return {
3393
+ status: "initialized",
3394
+ mode: "repo",
3395
+ message: initializedMessage({
3396
+ mode: "repo",
3397
+ folderId: folder.id,
3398
+ folderPath: folder.path,
3399
+ created: scaffold.created,
3400
+ sessionPath: options.sessionPath
3401
+ }),
3402
+ folderPath: folder.path,
3403
+ wikiDir: scaffold.wikiDir,
3404
+ metadataDir: scaffold.metadataDir,
3405
+ sessionPath: options.sessionPath
3406
+ };
3407
+ }
3408
+ async function initWorkspaceMode(options) {
3409
+ const gitRoot = await findGitRoot(options.cwd);
3410
+ if (gitRoot) {
3411
+ return initWorkspaceFromGitRepo(options, await realpath5(gitRoot));
3412
+ }
3413
+ return initWorkspaceWithoutGitRepo(options);
3414
+ }
3415
+ async function initWorkspaceFromGitRepo(options, gitRoot) {
3416
+ const repoDirectoryName = path10.basename(gitRoot);
3417
+ const defaultWorkspaceName = `${repoDirectoryName}-workspace`;
3418
+ if ((options.yes || options.interactive === false) && !options.workspacePath && await isWeaveSourceRepo(gitRoot)) {
3419
+ throw new Error(
3420
+ "Refusing to adopt the Weave source repo without --workspace-path. Run this command from a disposable test repo, or pass an explicit workspace path."
3421
+ );
3422
+ }
3423
+ const workspaceName = await resolveWorkspaceName(options, defaultWorkspaceName);
3424
+ if (workspaceName === "cancelled") {
3425
+ return cancelledWorkspace(options.cwd, options.sessionPath);
3426
+ }
3427
+ const workspacePath = options.workspacePath ? path10.resolve(options.cwd, options.workspacePath) : path10.join(path10.dirname(gitRoot), slugify(workspaceName, "workspace"));
3428
+ const repoTargetPath = path10.join(workspacePath, repoDirectoryName);
3429
+ await assertPathMissing(workspacePath, "Workspace path already exists");
3430
+ await assertPathMissing(repoTargetPath, "Repo target path already exists");
3431
+ const remote = await getGitRemote(gitRoot);
3432
+ await createDirExclusive(workspacePath);
3433
+ await movePath(gitRoot, repoTargetPath);
3434
+ const resolvedWorkspacePath = await realpath5(workspacePath);
3435
+ const created = await scaffoldWorkspace({
3436
+ workspacePath: resolvedWorkspacePath,
3437
+ workspaceName,
3438
+ repos: {
3439
+ [slugify(repoDirectoryName, "repo")]: {
3440
+ path: repoDirectoryName,
3441
+ kind: options.folderKind ?? "app",
3442
+ ...remote ? { remote } : {}
3443
+ }
3444
+ }
3445
+ });
3446
+ const folder = workspaceFolder(resolvedWorkspacePath, workspaceName, options.folderId);
3447
+ const commitCreated = await createInitialWorkspaceCommit(resolvedWorkspacePath);
3448
+ const session = createCurrentSession(folder, options.now);
3449
+ await saveCurrentSession(session, options.sessionPath);
3450
+ return {
3451
+ status: "initialized",
3452
+ mode: "workspace",
3453
+ message: initializedMessage({
3454
+ mode: "workspace",
3455
+ folderId: folder.id,
3456
+ folderPath: folder.path,
3457
+ created,
3458
+ sessionPath: options.sessionPath,
3459
+ oldRepoPath: gitRoot,
3460
+ newRepoPath: repoTargetPath,
3461
+ commitCreated
3462
+ }),
3463
+ folderPath: folder.path,
3464
+ wikiDir: path10.join(resolvedWorkspacePath, "wiki"),
3465
+ metadataDir: path10.join(resolvedWorkspacePath, ".weave"),
3466
+ sessionPath: options.sessionPath,
3467
+ commitCreated,
3468
+ warning: commitCreated ? void 0 : "Initial workspace commit was not created. Run git status in the workspace to recover."
3469
+ };
3470
+ }
3471
+ async function initWorkspaceWithoutGitRepo(options) {
3472
+ const workspacePath = await resolveWorkspacePath(options);
3473
+ if (workspacePath === "cancelled") {
3474
+ return cancelledWorkspace(options.cwd, options.sessionPath);
3475
+ }
3476
+ const workspaceName = await resolveWorkspaceName(options, path10.basename(workspacePath));
3477
+ if (workspaceName === "cancelled") {
3478
+ return cancelledWorkspace(options.cwd, options.sessionPath);
3479
+ }
3480
+ const workspacePathExists = await pathExists(workspacePath);
3481
+ if (workspacePathExists) {
3482
+ const currentPath = await realpath5(options.cwd);
3483
+ const targetPath = await realpath5(workspacePath);
3484
+ const isCurrentPath = currentPath === targetPath;
3485
+ if (!isCurrentPath || !await isDirectoryEmpty(workspacePath)) {
3486
+ throw new Error(`Workspace path already exists and is not an empty current directory: ${workspacePath}`);
3487
+ }
3488
+ } else {
3489
+ await createDirExclusive(workspacePath);
3490
+ }
3491
+ const created = await scaffoldWorkspace({
3492
+ workspacePath,
3493
+ workspaceName,
3494
+ repos: {}
3495
+ });
3496
+ const folder = workspaceFolder(await realpath5(workspacePath), workspaceName, options.folderId);
3497
+ const commitCreated = await createInitialWorkspaceCommit(folder.path);
3498
+ const session = createCurrentSession(folder, options.now);
3499
+ await saveCurrentSession(session, options.sessionPath);
3500
+ return {
3501
+ status: "initialized",
3502
+ mode: "workspace",
3503
+ message: initializedMessage({
3504
+ mode: "workspace",
3505
+ folderId: folder.id,
3506
+ folderPath: folder.path,
3507
+ created,
3508
+ sessionPath: options.sessionPath,
3509
+ commitCreated
3510
+ }),
3511
+ folderPath: folder.path,
3512
+ wikiDir: path10.join(folder.path, "wiki"),
3513
+ metadataDir: path10.join(folder.path, ".weave"),
3514
+ sessionPath: options.sessionPath,
3515
+ commitCreated,
3516
+ warning: commitCreated ? void 0 : "Initial workspace commit was not created. Run git status in the workspace to recover."
3517
+ };
3518
+ }
3519
+ async function selectInitMode(options) {
3520
+ if (options.mode) {
3521
+ if (options.mode === "repo" || options.mode === "workspace") {
3522
+ return options.mode;
3523
+ }
3524
+ throw new Error(`Unsupported init mode: ${options.mode}. Expected "repo" or "workspace".`);
3525
+ }
3526
+ if (options.yes || options.interactive === false) {
3527
+ return "repo";
3528
+ }
3529
+ const answer = await prompts.select({
3530
+ message: "How should Weave initialize this folder?",
3531
+ options: [
3532
+ {
3533
+ value: "repo",
3534
+ label: "Repo mode",
3535
+ hint: "Use this when you want to code/reference only this repo."
3536
+ },
3537
+ {
3538
+ value: "workspace",
3539
+ label: "Workspace mode",
3540
+ hint: "Use this when you work across multiple repos or folders."
3541
+ }
3542
+ ],
3543
+ initialValue: "repo"
3544
+ });
3545
+ if (prompts.isCancel(answer)) {
3546
+ prompts.cancel("Cancelled.");
3547
+ return "cancelled";
3548
+ }
3549
+ return answer;
3550
+ }
3551
+ async function shouldReplaceSession(options) {
3552
+ if (options.yes || options.interactive === false) {
3553
+ return true;
3554
+ }
3555
+ const answer = await prompts.confirm({
3556
+ message: "A Weave session already exists. Start a new session from this folder?",
3557
+ initialValue: true
3558
+ });
3559
+ if (prompts.isCancel(answer)) {
3560
+ prompts.cancel("Cancelled.");
3561
+ return false;
3562
+ }
3563
+ return answer;
3564
+ }
3565
+ async function resolveWorkspaceName(options, defaultName) {
3566
+ if (options.workspaceName) {
3567
+ return options.workspaceName;
3568
+ }
3569
+ if (options.yes || options.interactive === false) {
3570
+ return defaultName;
3571
+ }
3572
+ const answer = await prompts.text({
3573
+ message: "Workspace name",
3574
+ placeholder: defaultName,
3575
+ defaultValue: defaultName
3576
+ });
3577
+ if (prompts.isCancel(answer)) {
3578
+ prompts.cancel("Cancelled.");
3579
+ return "cancelled";
3580
+ }
3581
+ return String(answer).trim() || defaultName;
3582
+ }
3583
+ async function resolveWorkspacePath(options) {
3584
+ if (options.workspacePath) {
3585
+ return path10.resolve(options.cwd, options.workspacePath);
3586
+ }
3587
+ if (options.yes || options.interactive === false) {
3588
+ throw new Error("Workspace path is required for workspace mode outside a git repo.");
3589
+ }
3590
+ const answer = await prompts.text({
3591
+ message: "Workspace path",
3592
+ placeholder: path10.join(path10.dirname(options.cwd), "my-workspace")
3593
+ });
3594
+ if (prompts.isCancel(answer)) {
3595
+ prompts.cancel("Cancelled.");
3596
+ return "cancelled";
3597
+ }
3598
+ const value = String(answer).trim();
3599
+ if (!value) {
3600
+ throw new Error("Workspace path is required for workspace mode outside a git repo.");
3601
+ }
3602
+ return path10.resolve(options.cwd, value);
3603
+ }
3604
+ async function writeRepoWorkspaceMetadata(folder) {
3605
+ return writeFileIfMissing(
3606
+ path10.join(folder.path, ".weave", "workspace.yml"),
3607
+ workspaceMetadataTemplate({
3608
+ mode: "repo",
3609
+ name: folder.name,
3610
+ repos: {}
3611
+ })
3612
+ );
3613
+ }
3614
+ async function scaffoldWorkspace(input) {
3615
+ await runGitRequired(["init"], input.workspacePath);
3616
+ const folder = workspaceFolder(input.workspacePath, input.workspaceName);
3617
+ const scaffold = await ensureWeaveScaffold({ folder });
3618
+ const created = [...scaffold.created];
3619
+ if (await writeFileIfMissing(
3620
+ path10.join(input.workspacePath, ".weave", "workspace.yml"),
3621
+ workspaceMetadataTemplate({
3622
+ mode: "workspace",
3623
+ name: input.workspaceName,
3624
+ repos: {}
3625
+ })
3626
+ )) {
3627
+ created.push(".weave/workspace.yml");
3628
+ }
3629
+ if (await writeFileIfMissing(
3630
+ path10.join(input.workspacePath, ".gitignore"),
3631
+ workspaceGitignoreBaseTemplate()
3632
+ )) {
3633
+ created.push(".gitignore");
3634
+ }
3635
+ for (const [id, repo] of Object.entries(input.repos)) {
3636
+ await registerRepoIntoWorkspace({
3637
+ workspacePath: input.workspacePath,
3638
+ id,
3639
+ relativePath: repo.path,
3640
+ kind: repo.kind,
3641
+ remote: repo.remote
3642
+ });
3643
+ }
3644
+ return created;
3645
+ }
3646
+ async function createInitialWorkspaceCommit(workspacePath) {
3647
+ try {
3648
+ await runGitRequired(["add", "."], workspacePath);
3649
+ await runGitRequired(["commit", "-m", "Initialize Weave workspace"], workspacePath);
3650
+ return true;
3651
+ } catch {
3652
+ return false;
3653
+ }
3654
+ }
3655
+ function workspaceFolder(workspacePath, workspaceName, folderId) {
3656
+ const id = folderId ?? slugify(workspaceName, "workspace");
3657
+ return {
3658
+ id,
3659
+ path: workspacePath,
3660
+ name: titleFromSlug(id) || workspaceName,
3661
+ kind: "workspace"
3662
+ };
3663
+ }
3664
+ async function assertPathMissing(targetPath, message) {
3665
+ if (await pathExists(targetPath)) {
3666
+ throw new Error(`${message}: ${targetPath}`);
3667
+ }
3668
+ }
3669
+ async function isWeaveSourceRepo(gitRoot) {
3670
+ try {
3671
+ const manifest = JSON.parse(await readFile10(path10.join(gitRoot, "package.json"), "utf8"));
3672
+ return manifest.name === "weave-it";
3673
+ } catch {
3674
+ return false;
3675
+ }
3676
+ }
3677
+ function workspaceMetadataTemplate(input) {
3678
+ return YAML8.stringify({
3679
+ version: 1,
3680
+ mode: input.mode,
3681
+ name: input.name,
3682
+ repos: input.repos
3683
+ });
3684
+ }
3685
+ function cancelledWorkspace(folderPath, sessionPath) {
3686
+ return {
3687
+ status: "cancelled",
3688
+ message: "Cancelled. Weave workspace was not initialized.",
3689
+ folderPath,
3690
+ wikiDir: "",
3691
+ metadataDir: "",
3692
+ sessionPath
3693
+ };
3694
+ }
3695
+ function initializedMessage(input) {
3696
+ const createdText = input.created.length > 0 ? input.created.join("\n ") : "No repo files changed";
3697
+ const modeText = input.mode === "workspace" ? "workspace mode" : "repo mode";
3698
+ const repoMoveText = input.oldRepoPath && input.newRepoPath ? `
3699
+ Adopted repo:
3700
+ From: ${input.oldRepoPath}
3701
+ To: ${input.newRepoPath}
3702
+ ` : "";
3703
+ const commitText = input.mode === "workspace" ? `
3704
+ Initial workspace commit:
3705
+ ${input.commitCreated ? "Created" : "Not created - run git status in the workspace to recover"}
3706
+ ` : "";
3707
+ const nextText = input.mode === "workspace" ? `Open this workspace path in your editor:
3708
+ ${input.folderPath}
3709
+
3710
+ Then run:
3711
+ weave workspace` : `weave add <path>
3712
+ weave workspace`;
3713
+ return `Initialized Weave in ${modeText} for folder: ${input.folderId}
3714
+
3715
+ Folder:
3716
+ ${input.folderPath}
3717
+
3718
+ Created:
3719
+ ${createdText}
3720
+ ${repoMoveText}${commitText}
3721
+
3722
+ Started current session:
3723
+ ${input.folderId}
3724
+
3725
+ Session state:
3726
+ ${input.sessionPath}
3727
+
3728
+ Next:
3729
+ ${nextText}`;
3730
+ }
3731
+
3732
+ // src/commands/init.ts
3733
+ function initCommand() {
3734
+ return new Command5("init").description("Initialize Weave in repo mode or workspace mode and start a new session.").option("--id <id>", "folder id").option("--kind <kind>", "folder kind", "app").option("--mode <mode>", "init mode: repo or workspace; defaults to repo with --yes").option("--workspace-name <name>", "workspace name for workspace mode").option("--workspace-path <path>", "workspace path for workspace mode outside a git repo").option("--yes", "accept defaults and skip prompts").action(
3735
+ async (options) => {
3736
+ const result3 = await initWorkspace({
3737
+ cwd: process.cwd(),
3738
+ folderId: options.id,
3739
+ folderKind: options.kind,
3740
+ mode: options.mode,
3741
+ workspaceName: options.workspaceName,
3742
+ workspacePath: options.workspacePath,
3743
+ yes: options.yes ?? false,
3744
+ interactive: true
3745
+ });
3746
+ process.stdout.write(`${result3.message}
3747
+ `);
3748
+ if (result3.status === "cancelled") {
3749
+ process.exitCode = 1;
3750
+ }
3751
+ }
3752
+ );
3753
+ }
3754
+
3755
+ // src/commands/skills.ts
3756
+ import { Command as Command6 } from "commander";
3757
+ function skillsCommand() {
3758
+ const command = new Command6("skills").description("List Weave skills.");
3759
+ command.command("list").description("List default skills shipped with Weave.").option("--json", "print machine-readable JSON").action(async (options) => {
3760
+ await runAction4(async () => {
3761
+ const skills = await listDefaultSkills();
3762
+ if (options.json) {
3763
+ process.stdout.write(`${JSON.stringify(skills, null, 2)}
3764
+ `);
3765
+ return;
3766
+ }
3767
+ process.stdout.write(
3768
+ `${skills.map((skill) => `${skill.name} ${skill.description}`).join("\n")}
3769
+ `
3770
+ );
3771
+ });
3772
+ });
3773
+ return command;
3774
+ }
3775
+ function skillCommand() {
3776
+ const command = new Command6("skill").description("Show Weave skill content.");
3777
+ command.command("show").description("Print a default skill shipped with Weave.").argument("<name>", "skill name").action(async (name) => {
3778
+ await runAction4(async () => {
3779
+ const skill = await readDefaultSkill(name);
3780
+ process.stdout.write(skill.content);
3781
+ if (!skill.content.endsWith("\n")) {
3782
+ process.stdout.write("\n");
3783
+ }
3784
+ });
3785
+ });
3786
+ return command;
3787
+ }
3788
+ async function runAction4(action) {
3789
+ try {
3790
+ await action();
3791
+ } catch (error) {
3792
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
3793
+ `);
3794
+ process.exitCode = 1;
3795
+ }
3796
+ }
3797
+
3798
+ // src/commands/status.ts
3799
+ import { Command as Command7 } from "commander";
3800
+
3801
+ // src/lib/status.ts
3802
+ import { createHash as createHash4 } from "crypto";
3803
+ import { readFile as readFile11 } from "fs/promises";
3804
+ import { fileURLToPath as fileURLToPath3 } from "url";
3805
+ import { dirname as dirname4, join as join5 } from "path";
3806
+ var agentOrder = ["claude", "codex", "cursor", "opencode"];
3807
+ async function buildStatus(options) {
3808
+ const packageVersion = options.packageVersion ?? await readPackageVersion2();
3809
+ const inRepo = await pathExists(join5(options.cwd, ".weave", "agents.yml"));
3810
+ const skills = inRepo ? await collectSkillRows(options) : [];
3811
+ const npmLatest = options.npmLatest === void 0 ? (await getNpmVersionInfo({ packageVersion, env: options.env })).cachedLatest : options.npmLatest;
3812
+ const notices = await gatherNotices({
3813
+ cwd: options.cwd,
3814
+ packageVersion,
3815
+ npmLatest: npmLatest ?? null,
3816
+ templatesDir: options.templatesDir,
3817
+ env: options.env
3818
+ });
3819
+ return {
3820
+ status: "ok",
3821
+ packageVersion,
3822
+ cwd: options.cwd,
3823
+ inRepo,
3824
+ skills,
3825
+ notices,
3826
+ message: renderStatusMessage({
3827
+ packageVersion,
3828
+ cwd: options.cwd,
3829
+ inRepo,
3830
+ skills,
3831
+ notices,
3832
+ npmLatest: npmLatest ?? null
3833
+ })
3834
+ };
3835
+ }
3836
+ async function collectSkillRows(options) {
3837
+ const manifest = await loadAgentsManifest(options.cwd);
3838
+ const templates = await listDefaultSkills({ templatesDir: options.templatesDir }).catch(() => []);
3839
+ const templatesByName = new Map(templates.map((skill) => [skill.name, skill]));
3840
+ const rows = [];
3841
+ for (const agent of agentOrder) {
3842
+ const buckets = manifest.installed[agent];
3843
+ if (!buckets) continue;
3844
+ const skills = buckets.skills ?? {};
3845
+ for (const [name, entry] of Object.entries(skills)) {
3846
+ const template = templatesByName.get(name);
3847
+ const current = template?.lastChangedIn ?? "unknown";
3848
+ const absolutePath = join5(options.cwd, entry.path);
3849
+ const fileExists = await pathExists(absolutePath);
3850
+ let state = "current";
3851
+ if (!fileExists) {
3852
+ state = "missing";
3853
+ } else {
3854
+ const diskContent = await readFile11(absolutePath, "utf8").catch(() => "");
3855
+ const diskHash = `sha256:${createHash4("sha256").update(diskContent).digest("hex")}`;
3856
+ if (diskHash !== entry.installed_hash) {
3857
+ state = "modified";
3858
+ } else if (template && entry.installed_from !== template.lastChangedIn) {
3859
+ state = "outdated";
3860
+ }
3861
+ }
3862
+ rows.push({
3863
+ agent,
3864
+ name,
3865
+ installed_from: entry.installed_from,
3866
+ current,
3867
+ state
3868
+ });
3869
+ }
3870
+ }
3871
+ return rows;
3872
+ }
3873
+ function renderStatusMessage(opts) {
3874
+ const lines = [];
3875
+ lines.push(`weave-it ${opts.packageVersion}`);
3876
+ lines.push(`cwd: ${opts.cwd}`);
3877
+ if (opts.npmLatest) {
3878
+ lines.push(`npm latest (cached): ${opts.npmLatest}`);
3879
+ } else {
3880
+ lines.push(`npm latest (cached): unknown`);
3881
+ }
3882
+ if (!opts.inRepo) {
3883
+ lines.push("");
3884
+ lines.push("Not inside a Weave-managed repo (no .weave/agents.yml).");
3885
+ } else if (opts.skills.length === 0) {
3886
+ lines.push("");
3887
+ lines.push("No skills installed.");
3888
+ } else {
3889
+ lines.push("");
3890
+ lines.push("Installed skills:");
3891
+ const header = ["agent", "skill", "state", "installed_from", "current"];
3892
+ const rows = opts.skills.map((row) => [
3893
+ row.agent,
3894
+ row.name,
3895
+ row.state,
3896
+ row.installed_from ?? "unknown",
3897
+ row.current
3898
+ ]);
3899
+ const widths = header.map(
3900
+ (cell, index) => Math.max(cell.length, ...rows.map((row) => row[index].length))
3901
+ );
3902
+ const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd();
3903
+ lines.push(renderRow(header));
3904
+ lines.push(widths.map((width) => "-".repeat(width)).join(" ").trimEnd());
3905
+ for (const row of rows) {
3906
+ lines.push(renderRow(row));
3907
+ }
3908
+ }
3909
+ lines.push("");
3910
+ if (opts.notices.length === 0) {
3911
+ lines.push("Notices: none.");
3912
+ } else {
3913
+ lines.push(`Notices (${opts.notices.length}):`);
3914
+ for (const notice of opts.notices) {
3915
+ lines.push(`- [${notice.kind}] ${notice.message}`);
3916
+ }
3917
+ }
3918
+ return lines.join("\n");
3919
+ }
3920
+ async function readPackageVersion2() {
3921
+ const moduleDir = dirname4(fileURLToPath3(import.meta.url));
3922
+ let current = moduleDir;
3923
+ while (true) {
3924
+ const candidate = join5(current, "package.json");
3925
+ if (await pathExists(candidate)) {
3926
+ try {
3927
+ const parsed = JSON.parse(await readFile11(candidate, "utf8"));
3928
+ if (typeof parsed?.version === "string") {
3929
+ return parsed.version;
3930
+ }
3931
+ } catch {
3932
+ }
3933
+ }
3934
+ const parent = dirname4(current);
3935
+ if (parent === current) break;
3936
+ current = parent;
3937
+ }
3938
+ return "0.0.0";
3939
+ }
3940
+
3941
+ // src/commands/status.ts
3942
+ function statusCommand() {
3943
+ return new Command7("status").description("Show installed weave-it package and skill version state.").option("--json", "print machine-readable JSON").action(async (options) => {
3944
+ await withNotices(
3945
+ { commandName: "status", json: options.json ?? false },
3946
+ async () => {
3947
+ const result3 = await buildStatus({ cwd: process.cwd() });
3948
+ const { message: _message, notices: _notices, ...rest } = result3;
3949
+ return {
3950
+ json: rest,
3951
+ text: result3.message
3952
+ };
3953
+ }
3954
+ );
3955
+ });
3956
+ }
3957
+
3958
+ // src/commands/task.ts
3959
+ import { Command as Command8, InvalidArgumentError as InvalidArgumentError4 } from "commander";
3960
+
3961
+ // src/lib/task-prepare.ts
3962
+ import { readFile as readFile13, writeFile as writeFile5 } from "fs/promises";
3963
+ import path12 from "path";
3964
+ import YAML9 from "yaml";
3965
+
3966
+ // src/lib/tasks.ts
3967
+ import { readFile as readFile12 } from "fs/promises";
3968
+ import path11 from "path";
3969
+ async function loadTasksForChange(changePath) {
3970
+ const tasksPath = path11.join(changePath, "tasks.md");
3971
+ if (!await pathExists(tasksPath)) {
3972
+ throw new ChangeCommandError("tasks_missing", "No tasks.md found for the active change. Run `weave-issues` first.", { path: tasksPath });
3973
+ }
3974
+ return parseTasksMarkdown(await readFile12(tasksPath, "utf8"));
3975
+ }
3976
+ function parseTasksMarkdown(content) {
3977
+ const headings = [...content.matchAll(/^## (T\d+):\s*(.+)$/gm)];
3978
+ const tasks = [];
3979
+ for (let index = 0; index < headings.length; index += 1) {
3980
+ const heading = headings[index];
3981
+ const next = headings[index + 1];
3982
+ const id = heading[1];
3983
+ const title = heading[2].trim();
3984
+ const start = heading.index ?? 0;
3985
+ const end = next?.index ?? content.length;
3986
+ const block = content.slice(start, end);
3987
+ const fields = parseTaskFields(block);
3988
+ tasks.push({
3989
+ id,
3990
+ title,
3991
+ status: fields.get("status"),
3992
+ type: fields.get("type"),
3993
+ scope: fields.get("scope"),
3994
+ primaryRepo: fields.get("primary repo"),
3995
+ repos: parseRepoList(fields.get("repos"))
3996
+ });
3997
+ }
3998
+ return tasks;
3999
+ }
4000
+ function selectTasks(tasks, selector) {
4001
+ const availableTaskIds = tasks.map((task) => task.id);
4002
+ const availableScopes = unique(tasks.map((task) => task.scope).filter((scope) => Boolean(scope)));
4003
+ if (selector.type === "all") {
4004
+ return { selector, tasks, availableTaskIds, availableScopes };
4005
+ }
4006
+ if (selector.type === "scope") {
4007
+ const scope = selector.scope.toLowerCase();
4008
+ return {
4009
+ selector,
4010
+ tasks: tasks.filter((task) => task.scope?.toLowerCase() === scope),
4011
+ availableTaskIds,
4012
+ availableScopes
4013
+ };
4014
+ }
4015
+ const normalized = selector.ids.map((id) => id.toUpperCase());
4016
+ const found = tasks.filter((task) => normalized.includes(task.id.toUpperCase()));
4017
+ const foundIds = new Set(found.map((task) => task.id.toUpperCase()));
4018
+ const missing = normalized.filter((id) => !foundIds.has(id));
4019
+ if (missing.length > 0) {
4020
+ throw new ChangeCommandError("task_not_found", `No matching task found: ${missing.join(", ")}`, { missing, availableTaskIds });
4021
+ }
4022
+ return { selector, tasks: found, availableTaskIds, availableScopes };
4023
+ }
4024
+ function deriveTaskRepoIds(task) {
4025
+ return unique([
4026
+ ...parseRepoList(task.primaryRepo),
4027
+ ...task.repos
4028
+ ]);
4029
+ }
4030
+ function parseTaskFields(block) {
4031
+ const fields = /* @__PURE__ */ new Map();
4032
+ for (const line of block.split("\n")) {
4033
+ const match = line.match(/^([A-Za-z][A-Za-z ]+):\s*(.*)$/);
4034
+ if (!match) {
4035
+ continue;
4036
+ }
4037
+ fields.set(match[1].trim().toLowerCase(), cleanValue(match[2]));
4038
+ }
4039
+ return fields;
4040
+ }
4041
+ function parseRepoList(value) {
4042
+ if (!value) {
4043
+ return [];
4044
+ }
4045
+ return value.split(",").map((part) => cleanValue(part)).filter((part) => part.length > 0 && !isNoRepoValue(part));
4046
+ }
4047
+ function cleanValue(value) {
4048
+ return value.trim().replace(/^`+|`+$/g, "").trim();
4049
+ }
4050
+ function isNoRepoValue(value) {
4051
+ const normalized = value.toLowerCase();
4052
+ return normalized === "none" || normalized === "n/a" || normalized === "not applicable" || normalized === "-";
4053
+ }
4054
+ function unique(values) {
4055
+ const seen = /* @__PURE__ */ new Set();
4056
+ const out = [];
4057
+ for (const value of values) {
4058
+ if (seen.has(value)) {
4059
+ continue;
4060
+ }
4061
+ seen.add(value);
4062
+ out.push(value);
4063
+ }
4064
+ return out;
4065
+ }
4066
+
4067
+ // src/lib/task-prepare.ts
4068
+ async function prepareTasks(options) {
4069
+ const now = options.now ?? /* @__PURE__ */ new Date();
4070
+ const context = await activeChangeContext({ cwd: options.cwd, now, sessionPath: options.sessionPath });
4071
+ const branch = context.change.branch;
4072
+ const tasks = await loadTasksForChange(context.change.changePath);
4073
+ const selection = selectTasks(tasks, options.selector);
4074
+ const selector = formatSelector(options.selector);
4075
+ const selectedTasks = selection.tasks.map((task) => ({ id: task.id, title: task.title, scope: task.scope }));
4076
+ const blockers = [];
4077
+ const emptySelectionBlocker = selectionBlocker(selection.tasks.length, options.selector);
4078
+ if (emptySelectionBlocker) {
4079
+ blockers.push(emptySelectionBlocker);
4080
+ }
4081
+ const artifactRootBlocker = await artifactRootBranchBlocker(context.target.path, branch);
4082
+ if (artifactRootBlocker) {
4083
+ blockers.push(artifactRootBlocker);
4084
+ }
4085
+ const targets = context.mode === "repo" ? repoModeTargets(context.target.path, selection.tasks) : await workspaceModeTargets(context.target.path, selection.tasks, blockers);
4086
+ const plans = blockers.length === 0 ? await preflightTargets(targets, branch, blockers) : [];
4087
+ if (blockers.length > 0) {
4088
+ return result2({ status: "blocked", context, selector, tasks: selectedTasks, repos: [], blockers });
4089
+ }
4090
+ const repos = await applyPlans(plans, branch);
4091
+ if (repos.length > 0) {
4092
+ await writeExecutionState(context.change.changePath, branch, repos, now);
4093
+ }
4094
+ return result2({ status: "ok", context, selector, tasks: selectedTasks, repos, blockers: [] });
4095
+ }
4096
+ function repoModeTargets(rootPath, tasks) {
4097
+ if (tasks.length === 0) {
4098
+ return [];
4099
+ }
4100
+ return [{ id: "root", absolutePath: rootPath, relativePath: ".", mode: "repo" }];
4101
+ }
4102
+ async function workspaceModeTargets(rootPath, tasks, blockers) {
4103
+ const metadata = await readWorkspaceMetadata(rootPath);
4104
+ if (!metadata) {
4105
+ blockers.push({ target: rootPath, reason: "Workspace metadata is missing or invalid." });
4106
+ return [];
4107
+ }
4108
+ const repoIds = [];
4109
+ for (const task of tasks) {
4110
+ const taskRepoIds = deriveTaskRepoIds(task).filter((repoId) => repoId.toLowerCase() !== "workspace");
4111
+ if (taskRepoIds.length === 0) {
4112
+ blockers.push({ target: task.id, reason: "Task has no concrete repo metadata for workspace prepare." });
4113
+ continue;
4114
+ }
4115
+ for (const repoId of taskRepoIds) {
4116
+ if (!repoIds.includes(repoId)) {
4117
+ repoIds.push(repoId);
4118
+ }
4119
+ }
4120
+ }
4121
+ const targets = [];
4122
+ for (const repoId of repoIds) {
4123
+ const entry = metadata.repos[repoId];
4124
+ if (!entry) {
4125
+ blockers.push({ target: repoId, reason: "Task references a repo id that is not registered in workspace metadata." });
4126
+ continue;
4127
+ }
4128
+ const absolutePath = path12.join(rootPath, entry.path);
4129
+ if (!await pathExists(absolutePath)) {
4130
+ blockers.push({ target: repoId, reason: `Registered repo path does not exist: ${entry.path}` });
4131
+ continue;
4132
+ }
4133
+ targets.push({ id: repoId, absolutePath, relativePath: entry.path, mode: "workspace" });
4134
+ }
4135
+ return targets;
4136
+ }
4137
+ function selectionBlocker(taskCount, selector) {
4138
+ if (taskCount > 0) {
4139
+ return void 0;
4140
+ }
4141
+ if (selector.type === "scope") {
4142
+ return { target: selector.scope, reason: "No tasks matched the requested scope." };
4143
+ }
4144
+ if (selector.type === "all") {
4145
+ return { target: "all", reason: "No T# tasks were found in tasks.md." };
4146
+ }
4147
+ return void 0;
4148
+ }
4149
+ async function artifactRootBranchBlocker(rootPath, branch) {
4150
+ const gitRoot = await findGitRoot(rootPath);
4151
+ if (!gitRoot) {
4152
+ return void 0;
4153
+ }
4154
+ const current = await currentBranch(rootPath);
4155
+ if (!current) {
4156
+ return { target: rootPath, reason: "Artifact root is in detached HEAD or has no current branch." };
4157
+ }
4158
+ if (current !== branch) {
4159
+ return { target: rootPath, reason: `Artifact root branch is ${current}; expected ${branch}.` };
4160
+ }
4161
+ return void 0;
4162
+ }
4163
+ async function preflightTargets(targets, branch, blockers) {
4164
+ const plans = [];
4165
+ for (const target of targets) {
4166
+ const gitRoot = await findGitRoot(target.absolutePath);
4167
+ if (!gitRoot) {
4168
+ plans.push({ ...target, state: "skipped", branchStatus: "skipped_not_git" });
4169
+ continue;
4170
+ }
4171
+ const current = await currentBranch(target.absolutePath);
4172
+ if (!current) {
4173
+ blockers.push({ target: target.id, reason: "Repo is in detached HEAD or has no current branch." });
4174
+ continue;
4175
+ }
4176
+ if (current === branch) {
4177
+ plans.push({ ...target, state: "prepared", branchStatus: "already_active" });
4178
+ continue;
4179
+ }
4180
+ if (await isWorktreeDirty(target.absolutePath)) {
4181
+ blockers.push({ target: target.id, reason: `Repo has uncommitted changes on ${current}; expected ${branch}.` });
4182
+ continue;
4183
+ }
4184
+ plans.push({
4185
+ ...target,
4186
+ state: "prepared",
4187
+ branchStatus: await branchExists(target.absolutePath, branch) ? "checked_out" : "created"
4188
+ });
4189
+ }
4190
+ return blockers.length > 0 ? [] : plans;
4191
+ }
4192
+ async function applyPlans(plans, branch) {
4193
+ const repos = [];
4194
+ for (const plan of plans) {
4195
+ if (plan.branchStatus === "checked_out") {
4196
+ await checkoutBranch(plan.absolutePath, branch);
4197
+ } else if (plan.branchStatus === "created") {
4198
+ await createBranch(plan.absolutePath, branch);
4199
+ }
4200
+ const head = plan.state === "prepared" ? await currentHead(plan.absolutePath) : void 0;
4201
+ repos.push({
4202
+ id: plan.id,
4203
+ path: plan.relativePath,
4204
+ mode: plan.mode,
4205
+ state: plan.state,
4206
+ branch_status: plan.branchStatus,
4207
+ branch,
4208
+ ...head ? { prepared_head: head } : {}
4209
+ });
4210
+ }
4211
+ return repos;
4212
+ }
4213
+ async function writeExecutionState(changePath, branch, repos, now) {
4214
+ const statusPath = path12.join(changePath, "status.yml");
4215
+ const raw = YAML9.parse(await readFile13(statusPath, "utf8"));
4216
+ const existingExecution = isRecord2(raw.execution) ? raw.execution : void 0;
4217
+ const existingBranch = typeof existingExecution?.branch === "string" ? existingExecution.branch : void 0;
4218
+ const preserveExisting = existingBranch === branch && isRecord2(existingExecution?.repos);
4219
+ const existingRepos = preserveExisting ? existingExecution?.repos : {};
4220
+ const nextRepos = { ...existingRepos };
4221
+ const timestamp = now.toISOString();
4222
+ for (const repo of repos) {
4223
+ const existing = isRecord2(nextRepos[repo.id]) ? nextRepos[repo.id] : void 0;
4224
+ const existingPreparedAt = existing?.branch === branch && typeof existing.prepared_at === "string" ? existing.prepared_at : void 0;
4225
+ nextRepos[repo.id] = {
4226
+ path: repo.path,
4227
+ mode: repo.mode,
4228
+ branch: repo.branch,
4229
+ state: repo.state,
4230
+ branch_status: repo.branch_status,
4231
+ ...repo.prepared_head ? { prepared_head: repo.prepared_head } : {},
4232
+ prepared_at: existingPreparedAt ?? timestamp,
4233
+ verified_at: timestamp
4234
+ };
4235
+ }
4236
+ raw.execution = {
4237
+ version: 1,
4238
+ branch,
4239
+ repos: nextRepos
4240
+ };
4241
+ raw.updated_at = timestamp;
4242
+ await writeFile5(statusPath, YAML9.stringify(raw));
4243
+ }
4244
+ function result2(input) {
4245
+ const value = {
4246
+ status: input.status,
4247
+ change: { id: input.context.change.id, branch: input.context.change.branch, path: input.context.change.path },
4248
+ selector: input.selector,
4249
+ mode: input.context.mode,
4250
+ tasks: input.tasks,
4251
+ repos: input.repos,
4252
+ blockers: input.blockers,
4253
+ message: ""
4254
+ };
4255
+ value.message = formatPrepareMessage(value);
4256
+ return value;
4257
+ }
4258
+ function formatSelector(selector) {
4259
+ if (selector.type === "all") {
4260
+ return { type: "all" };
4261
+ }
4262
+ if (selector.type === "scope") {
4263
+ return { type: "scope", value: selector.scope };
4264
+ }
4265
+ return { type: "tasks", values: selector.ids };
4266
+ }
4267
+ function formatPrepareMessage(result3) {
4268
+ const lines = [
4269
+ `Task prepare: ${result3.change.id}`,
4270
+ `Branch: ${result3.change.branch}`,
4271
+ `Mode: ${result3.mode}`,
4272
+ `Status: ${result3.status}`
4273
+ ];
4274
+ if (result3.tasks.length > 0) {
4275
+ lines.push(`Tasks: ${result3.tasks.map((task) => task.id).join(", ")}`);
4276
+ }
4277
+ if (result3.repos.length > 0) {
4278
+ lines.push("", "Repos:", ...result3.repos.map((repo) => ` ${repo.id} ${repo.branch_status} ${repo.path}`));
4279
+ }
4280
+ if (result3.blockers.length > 0) {
4281
+ lines.push("", "Blockers:", ...result3.blockers.map((blocker) => ` ${blocker.target}: ${blocker.reason}`));
4282
+ }
4283
+ return lines.join("\n");
4284
+ }
4285
+ function isRecord2(value) {
4286
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
4287
+ }
4288
+
4289
+ // src/commands/task.ts
4290
+ function taskCommand() {
4291
+ const command = new Command8("task").description("Prepare and inspect Weave local tasks.");
4292
+ command.command("prepare").description("Prepare local branches for selected tasks.").argument("[tasks...]", "task ids, e.g. T1 T3").option("--scope <scope>", "prepare repos referenced by tasks with this scope").option("--all", "prepare repos referenced by all T# tasks").option("--json", "print machine-readable JSON").action(async (tasks, options) => {
4293
+ await runAction5(options.json ?? false, async () => {
4294
+ const selector = parsePrepareSelector(tasks, options);
4295
+ const result3 = await prepareTasks({ cwd: process.cwd(), selector });
4296
+ writeResult3(result3, options.json ?? false);
4297
+ if (result3.status === "blocked") {
4298
+ process.exitCode = 1;
4299
+ }
4300
+ });
4301
+ });
4302
+ return command;
4303
+ }
4304
+ function parsePrepareSelector(tasks, options) {
4305
+ const hasTasks = tasks.length > 0;
4306
+ const hasScope = typeof options.scope === "string" && options.scope.trim().length > 0;
4307
+ const hasAll = options.all === true;
4308
+ const modes = [hasTasks, hasScope, hasAll].filter(Boolean).length;
4309
+ if (modes !== 1) {
4310
+ throw new InvalidArgumentError4("Provide exactly one selector: task ids, --scope <scope>, or --all.");
4311
+ }
4312
+ if (hasAll) {
4313
+ return { type: "all" };
4314
+ }
4315
+ if (hasScope) {
4316
+ return { type: "scope", scope: options.scope?.trim() ?? "" };
4317
+ }
4318
+ return { type: "tasks", ids: tasks.map((task) => task.toUpperCase()) };
4319
+ }
4320
+ function writeResult3(result3, json) {
4321
+ if (json) {
4322
+ process.stdout.write(`${JSON.stringify(result3, null, 2)}
4323
+ `);
4324
+ return;
4325
+ }
4326
+ process.stdout.write(`${result3.message}
4327
+ `);
4328
+ }
4329
+ async function runAction5(json, action) {
4330
+ try {
4331
+ await action();
4332
+ } catch (error) {
4333
+ if (json) {
4334
+ process.stdout.write(`${JSON.stringify(errorResult3(error), null, 2)}
4335
+ `);
4336
+ } else {
4337
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
4338
+ `);
4339
+ }
4340
+ process.exitCode = 1;
4341
+ }
4342
+ }
4343
+ function errorResult3(error) {
4344
+ if (error instanceof ChangeCommandError) {
4345
+ return {
4346
+ status: "error",
4347
+ code: error.code,
4348
+ message: error.message,
4349
+ details: error.details
4350
+ };
4351
+ }
4352
+ if (error instanceof InvalidArgumentError4) {
4353
+ return {
4354
+ status: "error",
4355
+ code: "invalid_arguments",
4356
+ message: error.message
4357
+ };
4358
+ }
4359
+ return {
4360
+ status: "error",
4361
+ code: "unknown_error",
4362
+ message: error instanceof Error ? error.message : String(error)
4363
+ };
4364
+ }
4365
+
4366
+ // src/commands/workspace.ts
4367
+ import { Command as Command9 } from "commander";
4368
+
4369
+ // src/lib/show-workspace.ts
4370
+ import path13 from "path";
4371
+ async function showWorkspace(options = {}) {
4372
+ const cwd = options.cwd ?? process.cwd();
4373
+ const sessionPath = options.sessionPath ?? defaultSessionPath();
4374
+ const session = await loadCurrentSession(sessionPath);
4375
+ const modeResult = await findWorkspaceMode(cwd);
4376
+ if (isWorkspaceMode(modeResult)) {
4377
+ return buildWorkspaceModeResult({
4378
+ workspacePath: modeResult.workspacePath,
4379
+ session,
4380
+ json: options.json ?? false
4381
+ });
4382
+ }
4383
+ return buildRepoModeResult({ session, json: options.json ?? false });
4384
+ }
4385
+ async function buildWorkspaceModeResult(input) {
4386
+ const metadata = await readWorkspaceMetadata(input.workspacePath);
4387
+ const summary = workspaceSummary(input.workspacePath, metadata);
4388
+ const repos = metadata ? await reposWithAvailability(input.workspacePath, listReposForDisplay(metadata)) : [];
4389
+ const json = {
4390
+ session: sessionJson(input.session),
4391
+ workspace: summary,
4392
+ repos,
4393
+ folders: []
4394
+ };
4395
+ const text2 = workspaceText(summary, repos);
4396
+ return {
4397
+ status: "ok",
4398
+ message: input.json ? JSON.stringify(json, null, 2) : text2,
4399
+ json,
4400
+ text: text2
4401
+ };
4402
+ }
4403
+ function buildRepoModeResult(input) {
4404
+ if (!input.session) {
4405
+ const json2 = {
4406
+ session: null,
4407
+ workspace: null,
4408
+ repos: [],
4409
+ folders: []
4410
+ };
4411
+ const text3 = "No current Weave session found. Run `weave init` first.";
4412
+ return {
4413
+ status: "no_session",
4414
+ message: input.json ? JSON.stringify(json2, null, 2) : text3,
4415
+ json: json2,
4416
+ text: text3
4417
+ };
4418
+ }
4419
+ const folders = Object.entries(input.session.folders).map(
4420
+ ([id, folder]) => buildFolderOutput(id, folder.path, folder.kind)
4421
+ );
4422
+ const json = {
4423
+ session: sessionJson(input.session),
4424
+ workspace: null,
4425
+ repos: [],
4426
+ folders
4427
+ };
4428
+ const text2 = repoText(folders);
4429
+ return {
4430
+ status: "ok",
4431
+ message: input.json ? JSON.stringify(json, null, 2) : text2,
4432
+ json,
4433
+ text: text2
4434
+ };
4435
+ }
4436
+ function buildFolderOutput(id, folderPath, kind) {
4437
+ return {
4438
+ id,
4439
+ path: folderPath,
4440
+ kind,
4441
+ wiki: path13.join(folderPath, "wiki"),
4442
+ metadata: path13.join(folderPath, ".weave")
4443
+ };
4444
+ }
4445
+ function sessionJson(session) {
4446
+ if (!session) {
4447
+ return null;
4448
+ }
4449
+ return {
4450
+ status: "active",
4451
+ updated_at: session.updated_at
4452
+ };
4453
+ }
4454
+ function workspaceSummary(workspacePath, metadata) {
4455
+ return {
4456
+ name: metadata?.name ?? path13.basename(workspacePath),
4457
+ path: workspacePath,
4458
+ mode: "workspace"
4459
+ };
4460
+ }
4461
+ async function reposWithAvailability(workspacePath, repos) {
4462
+ return Promise.all(
4463
+ repos.map(async (repo) => ({
4464
+ ...repo,
4465
+ availability: await pathExists(path13.join(workspacePath, repo.path)) ? "present" : "missing"
4466
+ }))
4467
+ );
4468
+ }
4469
+ function workspaceText(summary, repos) {
4470
+ const repoLines = repos.length === 0 ? " (no repos registered yet)" : repos.map((repo) => {
4471
+ const remoteSuffix = repo.remote ? ` ${repo.remote}` : "";
4472
+ return ` ${repo.id} ${repo.path} ${repo.kind} ${repo.availability}${remoteSuffix}`;
4473
+ }).join("\n");
4474
+ return `Weave workspace: ${summary.name}
4475
+
4476
+ Path:
4477
+ ${summary.path}
4478
+
4479
+ Repos:
4480
+ ${repoLines}
4481
+
4482
+ Next:
4483
+ weave add <path|url>
4484
+ weave change new <title>`;
4485
+ }
4486
+ function repoText(folders) {
4487
+ const folderLines = folders.map((folder) => ` ${folder.id} ${folder.path} ${folder.kind}`).join("\n");
4488
+ return `Current Weave session
4489
+
4490
+ Folders:
4491
+ ${folderLines}
4492
+
4493
+ Next:
4494
+ weave add <path>
4495
+ weave init`;
4496
+ }
4497
+
4498
+ // src/commands/workspace.ts
4499
+ function workspaceCommand() {
4500
+ return new Command9("workspace").description("Show the current Weave workspace (workspace mode) or session folders (repo mode).").option("--json", "print machine-readable JSON").action(async (options) => {
4501
+ await withNotices(
4502
+ { commandName: "workspace", json: options.json ?? false },
4503
+ async () => {
4504
+ const result3 = await showWorkspace({ cwd: process.cwd() });
4505
+ return {
4506
+ json: result3.json,
4507
+ text: result3.text,
4508
+ exitCode: result3.status === "no_session" ? 1 : 0
4509
+ };
4510
+ }
4511
+ );
4512
+ });
4513
+ }
4514
+
4515
+ // src/cli.ts
4516
+ import { pathToFileURL } from "url";
4517
+ function createProgram() {
4518
+ const program = new Command10();
4519
+ program.name("weave").description("Repo-local LLM wiki and temporary multi-folder AI session tooling.").version("0.1.0");
4520
+ program.addCommand(initCommand());
4521
+ program.addCommand(addCommand());
4522
+ program.addCommand(workspaceCommand());
4523
+ program.addCommand(changeCommand());
4524
+ program.addCommand(artifactCommand());
4525
+ program.addCommand(agentCommand());
4526
+ program.addCommand(skillsCommand());
4527
+ program.addCommand(skillCommand());
4528
+ program.addCommand(statusCommand());
4529
+ program.addCommand(taskCommand());
4530
+ return program;
4531
+ }
4532
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
4533
+ await createProgram().parseAsync(process.argv);
4534
+ }
4535
+ export {
4536
+ createProgram
4537
+ };
4538
+ //# sourceMappingURL=cli.js.map