dw-kit 1.6.0-rc.1 → 1.7.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/dv-workflow/dv-workflow/blob/main/.dw/core/schemas/goal-frontmatter.schema.json",
4
+ "title": "goal@v1",
5
+ "description": "Frontmatter schema for .dw/goals/{goal-id}/goal.md (ADR-0010 Goals Management Layer, v1.7).",
6
+ "type": "object",
7
+ "required": [
8
+ "goal_id",
9
+ "schema_version",
10
+ "created",
11
+ "last_updated",
12
+ "status",
13
+ "owner",
14
+ "goal_version"
15
+ ],
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "goal_id": {
19
+ "type": "string",
20
+ "pattern": "^G-[A-Za-z0-9](?:[A-Za-z0-9.-]{0,31}[A-Za-z0-9])?$",
21
+ "description": "Goal identifier. Format G-{slug}; uniqueness enforced via goals-index.json (W-1)."
22
+ },
23
+ "schema_version": {
24
+ "type": "string",
25
+ "const": "goal@v1",
26
+ "description": "Schema version. Upgrade path via `dw goal migrate --to goal@v2` (S-1)."
27
+ },
28
+ "created": {
29
+ "type": "string",
30
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
31
+ "description": "ISO date YYYY-MM-DD"
32
+ },
33
+ "last_updated": {
34
+ "type": "string",
35
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
36
+ "description": "ISO date YYYY-MM-DD"
37
+ },
38
+ "status": {
39
+ "type": "string",
40
+ "enum": ["Draft", "Active", "Achieved", "Abandoned", "Pivoted"],
41
+ "description": "Q3 OKR-style lifecycle. Status transitions auto-bump goal_version (Q4/C-1)."
42
+ },
43
+ "owner": {
44
+ "type": "string",
45
+ "minLength": 1,
46
+ "description": "Person responsible for shipping this goal."
47
+ },
48
+ "target_date": {
49
+ "type": ["string", "null"],
50
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$|^TBD$",
51
+ "description": "ISO date YYYY-MM-DD or 'TBD' or null."
52
+ },
53
+ "goal_version": {
54
+ "type": "integer",
55
+ "minimum": 1,
56
+ "description": "Q4/C-1: Bumped only on manual `dw goal bump --reason` or status transition (goal_status_changed event). Field edits (KR progress, summary) emit goal_field_updated event without counter increment."
57
+ },
58
+ "archived_at": {
59
+ "type": ["string", "null"],
60
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$",
61
+ "description": "C-2: Soft-delete timestamp ISO UTC. null = active. Set automatically when status transitions to Abandoned or via `dw goal delete`."
62
+ },
63
+ "parent_goal_id": {
64
+ "type": ["string", "null"],
65
+ "pattern": "^G-[A-Za-z0-9](?:[A-Za-z0-9.-]{0,31}[A-Za-z0-9])?$|^none$",
66
+ "description": "Optional parent goal (for sub-goals). 'none' or null = top-level goal."
67
+ },
68
+ "summary": {
69
+ "type": ["string", "null"],
70
+ "maxLength": 1000,
71
+ "description": "≤1000-char short-form (agentchattr borrow). Mirrored to goals-index.json for O(1) portfolio reads + Tier 1 agent reaction."
72
+ },
73
+ "icon": {
74
+ "type": ["string", "null"],
75
+ "maxLength": 8,
76
+ "description": "Optional emoji/icon for visual differentiation in portfolio cards (bigokr borrow)."
77
+ },
78
+ "cycle": {
79
+ "type": ["string", "null"],
80
+ "maxLength": 32,
81
+ "description": "Optional temporal grouping label e.g. 'Q1 2026', 'v1.7-cycle', '2026 H1' (bigokr borrow). Portfolio groups goals by cycle."
82
+ }
83
+ }
84
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://github.com/dv-workflow/dv-workflow/blob/main/.dw/core/schemas/task-frontmatter.schema.json",
4
4
  "title": "task-frontmatter",
5
- "description": "Frontmatter schema for .dw/tasks/{name}/task.md (v3 task format per ADR-0008)",
5
+ "description": "Frontmatter schema for .dw/tasks/{name}/task.md (v3 task format per ADR-0008; v3.1 adds optional goal linkage + summary per ADR-0010)",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "task_id",
@@ -67,12 +67,31 @@
67
67
  },
68
68
  "schema_version": {
69
69
  "type": "string",
70
- "const": "v3.0",
71
- "description": "Frontmatter schema version. Bumped only on breaking schema change."
70
+ "enum": ["v3.0", "v3.1"],
71
+ "description": "Frontmatter schema version. v3.1 adds optional parent_goal_id, contributing_goal_ids, summary per ADR-0010."
72
72
  },
73
73
  "blockers": {
74
74
  "type": "string",
75
75
  "description": "Current blockers in free-form text; use 'none' if unblocked"
76
+ },
77
+ "parent_goal_id": {
78
+ "type": ["string", "null"],
79
+ "pattern": "^G-[A-Za-z0-9](?:[A-Za-z0-9.-]{0,31}[A-Za-z0-9])?$|^none$",
80
+ "description": "v3.1 (ADR-0010 Q1): Primary goal this task contributes to. Single goal; ownership semantics. Format G-{slug} or 'none' or null."
81
+ },
82
+ "contributing_goal_ids": {
83
+ "type": "array",
84
+ "items": {
85
+ "type": "string",
86
+ "pattern": "^G-[A-Za-z0-9](?:[A-Za-z0-9.-]{0,31}[A-Za-z0-9])?$"
87
+ },
88
+ "uniqueItems": true,
89
+ "description": "v3.1 (ADR-0010 Q1): Optional secondary goals task contributes to. Informational only; no ownership/portfolio-placement effect."
90
+ },
91
+ "summary": {
92
+ "type": ["string", "null"],
93
+ "maxLength": 1000,
94
+ "description": "v3.1 (ADR-0010 + agentchattr borrow): ≤1000-char short-form for portfolio cards + Tier 1 agent reaction. Mirrored to goals-index.json/active-index when applicable."
76
95
  }
77
96
  }
78
97
  }
@@ -0,0 +1,146 @@
1
+ ---
2
+ goal_id: G-{slug}
3
+ schema_version: goal@v1
4
+ created: {YYYY-MM-DD}
5
+ last_updated: {YYYY-MM-DD}
6
+ status: Draft
7
+ owner: {name}
8
+ target_date: {YYYY-MM-DD or TBD}
9
+ goal_version: 1
10
+ archived_at: null
11
+ parent_goal_id: none
12
+ icon: "🎯"
13
+ cycle: "{Q1 2026 | v1.7-cycle | 2026 H1 | null}"
14
+ summary: "{≤1000-char short-form for portfolio cards + agent reaction; mirrored to goals-index.json}"
15
+ ---
16
+
17
+ # Goal: {Title}
18
+
19
+ <!--
20
+ Goals Management Layer (ADR-0010 Accepted Round 2, 2026-05-22).
21
+
22
+ This template mirrors task.md v3 structure (dual-projection per ADR-0008) so
23
+ agents and humans navigate goal.md and task.md with the same mental model.
24
+
25
+ Status lives ONLY in Section 3 (Subtask/KR Tracker) — never in Section 2.
26
+ Status transitions auto-bump goal_version per Q4/C-1; non-status field edits
27
+ emit goal_field_updated event without counter increment.
28
+
29
+ Sidecar SVG (timeline-goal.svg) auto-rendered by `dw goal render` once
30
+ dw-kit-render `renderGoal()` ships (R-G6 telemetry-gated, post-P1).
31
+ -->
32
+
33
+ <!-- ![Goal Timeline](./goal.svg) -->
34
+
35
+ ## 1. Snapshot
36
+
37
+ **Status:** {Draft | Active | Achieved | Abandoned | Pivoted}
38
+ **Owner:** {name}
39
+ **Target date:** {YYYY-MM-DD or TBD}
40
+ **Goal version:** {1}
41
+ **Last updated:** {YYYY-MM-DD}
42
+ **Linked tasks:** {auto-populated from `dw goal lint` or `dw goal portfolio`}
43
+
44
+ ## 2. Statement
45
+
46
+ <!--
47
+ Section 2 is the stable intent contract. Per lint convention, status markers
48
+ (✅, 🟡, dates) MUST NOT appear in Section 2 — those live exclusively in
49
+ Section 3 (Tracker). Section 2 changes only when intent or scope genuinely
50
+ changes (auto-bumps goal_version via `dw goal bump --reason`).
51
+ -->
52
+
53
+ ### What
54
+
55
+ {1-2 paragraphs — what outcome this goal is chasing. No background padding.}
56
+
57
+ ### Why Now
58
+
59
+ {Forcing function: deadline, market signal, dependency, strategic bet.}
60
+
61
+ ### Out of Scope
62
+
63
+ - {Explicit exclusions — prevents goal scope creep into Jira-lite territory (R-G1)}
64
+
65
+ ## 3. Key Results & Linked Tasks Tracker
66
+
67
+ <!--
68
+ SINGLE SOURCE OF TRUTH for KR status. This is the only section where status
69
+ markers (⬜🟡✅🔴⏸) are allowed. KR progress is percentage 0-100% per Q2.
70
+ Linked tasks auto-populated from frontmatter parent_goal_id / contributing_goal_ids
71
+ in task.md v3.1.
72
+ -->
73
+
74
+ ### Key Results
75
+
76
+ | # | Key Result | Target | Current | Status | Notes |
77
+ |---|-----------|--------|---------|--------|-------|
78
+ | KR-001 | {measurable outcome} | {100%} | {0%} | ⬜ Pending | |
79
+ | KR-002 | ... | ... | ... | ⬜ Pending | |
80
+
81
+ Status legend: ⬜ Pending · 🟡 In Progress · ✅ Achieved · 🔴 Blocked · ⏸ Paused
82
+
83
+ ### Linked Tasks
84
+
85
+ | Task | Role | KR | Status |
86
+ |------|------|----|--------|
87
+ | {auto-populated by `dw goal lint`} | primary/contributing | KR-N | ... |
88
+
89
+ ## 4. Timeline / Changelog
90
+
91
+ <!--
92
+ Reverse-chronological. Each pivot or material change = one heading. Section
93
+ auto-rotates per goal_version bump (manual `dw goal bump --reason` or status
94
+ transition). Field edits emit events to events-global.jsonl WITHOUT bumping
95
+ this section (Q4/C-1 separation).
96
+ -->
97
+
98
+ ```mermaid
99
+ timeline
100
+ title Goal Lifecycle
101
+ {YYYY-MM-DD} : Drafted (v1)
102
+ ```
103
+
104
+ ### {YYYY-MM-DD} — Goal drafted (version 1)
105
+
106
+ **Statement:**
107
+ {snapshot of Section 2 at creation time for diff history}
108
+
109
+ **Key Results defined:**
110
+ - KR-001: ...
111
+ - KR-002: ...
112
+
113
+ **Initial target date:** {YYYY-MM-DD or TBD}
114
+
115
+ ## 5. Handoff & Annotations
116
+
117
+ <!--
118
+ Cross-session context for agents picking up work on this goal. Annotations
119
+ (canvas-style sticky notes) live in peer file goal-annotations.md per Q6
120
+ (ADR-0010 P6 scope).
121
+ -->
122
+
123
+ **For next session (or next agent):**
124
+
125
+ - **Read first:** {related ADRs, parent goal if any, linked task.md files}
126
+ - **Current state:** {phase, blocked-on, last decision}
127
+ - **Don't do:** {anti-patterns specific to this goal}
128
+ - **Watch out:** {strategic risks, dependencies, stakeholder commitments}
129
+
130
+ ### Friction Journal
131
+
132
+ | Date | Friction | Component | Proposed fix |
133
+ |------|----------|-----------|-------------|
134
+ | {YYYY-MM-DD} | ... | ... | ... |
135
+
136
+ ## 6. Annexes
137
+
138
+ <!--
139
+ Free-form supplements. Recommended names:
140
+ - baseline.md — initial metrics
141
+ - pivot-r{N}.md — pivot rationale
142
+ - goal-annotations.md — canvas annotations (P6+)
143
+ - timeline-history.md — auto-rotated overflow
144
+ -->
145
+
146
+ - (none)
package/CLAUDE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Workflow toolkit codebase. Rules live in `.claude/rules/` (auto-loaded).
4
4
 
5
5
  **v2.0 direction:** Context-First SDLC Governance Layer (5 pillars — see `.dw/core/PILLARS.md`)
6
- **Current:** v1.3.6 (released 2026-05-14) · ADR-0001 active · ADR-0005 + ADR-0006 Accepted (Supply-Chain Guard 3-pillar AI-Native; sunset review 2026-08-12 per pillar) · v1.4 cuts pending telemetry · **ADR-0008 Accepted (task docs v3 = 1-file `task.md` + 3-tier auto-sync sidecar)** shipped v1.5 (commit 2030dd2) · **ADR-0009 Accepted Round 2 (Agent OS multi-agent orchestration)** execution task `agent-os-orchestration` shipping v1.6 (rc.1 candidate)
6
+ **Current:** v1.6.0-rc.1 on npm `rc` tag (2026-05-20) + **v1.7.0-rc.1 candidate** on `feat/goals-okr-v1.7` shipping ADR-0010 Goals Management Layer (Accepted Round 2 all 4 Critical + 7 Warnings + 4 Suggestions resolved; `bcurts/agentchattr` summary primitive + bigokr icon/cycle/progress/constellation visualization borrowed; 150/150 smoke tests). Active ADRs: ADR-0001 (Pragmatic Lean), ADR-0005/0006 (Supply-Chain Guard; sunset review 2026-08-12), ADR-0008 (Task Docs v3, v1.5), ADR-0009 (Agent OS, v1.6), ADR-0010 (Goals Layer, v1.7). v1.4 cuts pending telemetry.
7
7
 
8
8
  ---
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dw-kit",
3
- "version": "1.6.0-rc.1",
3
+ "version": "1.7.0-rc.1",
4
4
  "description": "AI development workflow toolkit — structured, quality-assured, team-ready. From requirements to dashboard.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -121,13 +121,14 @@ export function run(argv) {
121
121
 
122
122
  taskCmd
123
123
  .command('migrate [task-name]')
124
- .description('Migrate v2 spec.md + tracking.md → v3 task.md')
124
+ .description('Migrate v2 spec.md + tracking.md → v3 task.md, OR bump v3.0 → v3.1 schema (--to-v3.1)')
125
125
  .option('-n, --dry-run', 'Preview without writing')
126
126
  .option('--diff', 'Show diff against existing task.md')
127
- .option('--all', 'Scan and migrate all v2 tasks')
127
+ .option('--all', 'Scan and migrate all v2 tasks (or all v3.0 tasks when --to-v3.1)')
128
128
  .option('--force', 'Overwrite existing task.md')
129
129
  .option('--remove-v2', 'Delete spec.md/tracking.md after backup (default keeps them)')
130
130
  .option('--rollback', 'Restore v2 files from .v2bak backups (requires task name)')
131
+ .option('--to-v3-1', 'Bump v3.0 → v3.1 schema (adds optional parent_goal_id, contributing_goal_ids, summary fields per ADR-0010)')
131
132
  .action(async (taskName, opts) => {
132
133
  const { taskMigrateCommand } = await import('./commands/task-migrate.mjs');
133
134
  await taskMigrateCommand(taskName, opts);
@@ -178,11 +179,21 @@ export function run(argv) {
178
179
  .command('watch [task-name]')
179
180
  .description('Watch task.md for changes and live-reload the HTML preview in browser (local server, debounced)')
180
181
  .option('-p, --port <port>', 'Local server port (auto-finds next available if busy)', parseInt)
182
+ .option('--rotate-token', 'Regenerate .dw/cache/watch.token at startup (invalidates old browser sessions; C-3)')
181
183
  .action(async (taskName, opts) => {
182
184
  const { taskWatchCommand } = await import('./commands/task-watch.mjs');
183
185
  await taskWatchCommand(taskName, opts);
184
186
  });
185
187
 
188
+ taskCmd
189
+ .command('summary <task-name>')
190
+ .description('Read or update task summary frontmatter field (≤1000 chars; agentchattr borrow)')
191
+ .option('--write <text>', 'Update summary (auto-bumps schema_version v3.0→v3.1 if needed)')
192
+ .action(async (taskName, opts) => {
193
+ const { taskSummaryCommand } = await import('./commands/task-summary.mjs');
194
+ await taskSummaryCommand(taskName, opts);
195
+ });
196
+
186
197
  const agentCmd = program
187
198
  .command('agent')
188
199
  .description('Agent OS multi-agent orchestration (ADR-0009): claim · release · claims · reports · conflicts');
@@ -254,6 +265,146 @@ export function run(argv) {
254
265
  await agentConflictsCommand(opts);
255
266
  });
256
267
 
268
+ const goalCmd = program
269
+ .command('goal')
270
+ .description('Goals Management Layer (ADR-0010): strategic layer above tasks. new · show · link · summary · portfolio · lint · bump · delete · view');
271
+
272
+ goalCmd
273
+ .command('new <goal-id>')
274
+ .description('Create a new goal from template (ID format G-{slug})')
275
+ .option('--title <text>', 'Goal title (default: "New Goal")')
276
+ .option('--owner <name>', 'Owner (default: current user)')
277
+ .option('--target-date <YYYY-MM-DD>', 'Target date (default: TBD)')
278
+ .option('--icon <emoji>', 'Icon/emoji for portfolio cards (default: 🎯)')
279
+ .option('--cycle <label>', 'Cycle label e.g. "Q2 2026", "v1.7-cycle"')
280
+ .option('--summary <text>', 'Initial ≤1000-char summary')
281
+ .option('--parent-goal-id <id>', 'Parent goal ID for sub-goals')
282
+ .action(async (goalId, opts) => {
283
+ const { goalNewCommand } = await import('./commands/goal-new.mjs');
284
+ await goalNewCommand(goalId, opts);
285
+ });
286
+
287
+ goalCmd
288
+ .command('set <goal-id>')
289
+ .description('Update goal metadata fields (icon, cycle, owner, target_date, parent_goal_id)')
290
+ .option('--icon <emoji>', 'Icon/emoji (empty string removes)')
291
+ .option('--cycle <label>', 'Cycle label (empty string removes)')
292
+ .option('--owner <name>', 'New owner')
293
+ .option('--target-date <YYYY-MM-DD>', 'New target date or "TBD"')
294
+ .option('--parent-goal-id <id>', 'Parent goal ID or "none"')
295
+ .action(async (goalId, opts) => {
296
+ const { goalSetCommand } = await import('./commands/goal-set.mjs');
297
+ await goalSetCommand(goalId, opts);
298
+ });
299
+
300
+ goalCmd
301
+ .command('show [goal-id]')
302
+ .description('ANSI snapshot of a goal (no arg = list all)')
303
+ .action(async (goalId, opts) => {
304
+ const { goalShowCommand } = await import('./commands/goal-show.mjs');
305
+ await goalShowCommand(goalId, opts);
306
+ });
307
+
308
+ goalCmd
309
+ .command('link <goal-id> <task-id>')
310
+ .description('Link a task → goal (primary parent_goal_id; --contributing for secondary)')
311
+ .option('--contributing', 'Add to contributing_goal_ids instead of parent_goal_id (Q1)')
312
+ .option('-f, --force', 'Override existing parent_goal_id')
313
+ .action(async (goalId, taskId, opts) => {
314
+ const { goalLinkCommand } = await import('./commands/goal-link.mjs');
315
+ await goalLinkCommand(goalId, taskId, opts);
316
+ });
317
+
318
+ goalCmd
319
+ .command('unlink <goal-id> <task-id>')
320
+ .description('Remove task→goal mapping (clears both parent_goal_id and contributing_goal_ids)')
321
+ .action(async (goalId, taskId) => {
322
+ const { goalUnlinkCommand } = await import('./commands/goal-link.mjs');
323
+ await goalUnlinkCommand(goalId, taskId);
324
+ });
325
+
326
+ goalCmd
327
+ .command('summary <goal-id>')
328
+ .description('Read or update goal summary (≤1000 chars; agentchattr borrow)')
329
+ .option('--write <text>', 'Update summary (≤1000 chars); auto-syncs to goals-index.json')
330
+ .action(async (goalId, opts) => {
331
+ const { goalSummaryCommand } = await import('./commands/goal-summary.mjs');
332
+ await goalSummaryCommand(goalId, opts);
333
+ });
334
+
335
+ goalCmd
336
+ .command('bump <goal-id>')
337
+ .description('Manually bump goal_version (Q4 material change; requires --reason)')
338
+ .option('--reason <text>', 'Reason for the version bump (required)')
339
+ .action(async (goalId, opts) => {
340
+ const { goalBumpCommand } = await import('./commands/goal-bump.mjs');
341
+ await goalBumpCommand(goalId, opts);
342
+ });
343
+
344
+ goalCmd
345
+ .command('delete <goal-id>')
346
+ .description('Delete a goal (C-2: soft by default, --hard removes folder)')
347
+ .option('--cascade', 'Clear parent_goal_id / contributing_goal_ids on linked tasks')
348
+ .option('--hard', 'Hard-delete: remove .dw/goals/{id}/ folder (vs soft = status→Abandoned)')
349
+ .option('-n, --dry-run', 'Preview without writing')
350
+ .action(async (goalId, opts) => {
351
+ const { goalDeleteCommand } = await import('./commands/goal-delete.mjs');
352
+ await goalDeleteCommand(goalId, opts);
353
+ });
354
+
355
+ goalCmd
356
+ .command('lint [goal-id]')
357
+ .description('Lint goal.md: frontmatter schema + summary length + index drift + orphan cross-refs')
358
+ .action(async (goalId, opts) => {
359
+ const { goalLintCommand } = await import('./commands/goal-lint.mjs');
360
+ await goalLintCommand(goalId, opts);
361
+ });
362
+
363
+ goalCmd
364
+ .command('portfolio')
365
+ .description('Show all goals at-a-glance (O(1) read via goals-index.json; W-1)')
366
+ .option('--status <status>', 'Filter by status (Draft | Active | Achieved | Abandoned | Pivoted)')
367
+ .action(async (opts) => {
368
+ const { goalPortfolioCommand } = await import('./commands/goal-portfolio.mjs');
369
+ await goalPortfolioCommand(opts);
370
+ });
371
+
372
+ goalCmd
373
+ .command('view [goal-id]')
374
+ .description('Generate HTML portfolio (P1+P2 joint MVP per W-7)')
375
+ .option('--no-open', 'Do not auto-open browser')
376
+ .action(async (goalId, opts) => {
377
+ const { goalViewCommand } = await import('./commands/goal-view.mjs');
378
+ await goalViewCommand(goalId, opts);
379
+ });
380
+
381
+ goalCmd
382
+ .command('migrate [goal-id]')
383
+ .description('Stub for future schema bump (S-1; goal@v1 → goal@v2)')
384
+ .option('--to <schema>', 'Target schema (default: goal@v2)')
385
+ .action(async (goalId, opts) => {
386
+ const { goalMigrateCommand } = await import('./commands/goal-stubs.mjs');
387
+ await goalMigrateCommand(goalId, opts);
388
+ });
389
+
390
+ goalCmd
391
+ .command('suggest-krs <goal-id>')
392
+ .description('Build SMART-formatted prompt to paste into AI agent chat for KR brainstorming (W-5)')
393
+ .option('--count <n>', 'Requested number of KRs (default: 3)', '3')
394
+ .option('--json', 'Output structured JSON (for agent piping)')
395
+ .action(async (goalId, opts) => {
396
+ const { goalSuggestKrsCommand } = await import('./commands/goal-suggest-krs.mjs');
397
+ await goalSuggestKrsCommand(goalId, opts);
398
+ });
399
+
400
+ goalCmd
401
+ .command('render [goal-id]')
402
+ .description('Render constellation SVG for goal(s) — goal center + KR satellites + task leaves (R-G6 shipped)')
403
+ .action(async (goalId, opts) => {
404
+ const { goalRenderCommand } = await import('./commands/goal-render.mjs');
405
+ await goalRenderCommand(goalId, opts);
406
+ });
407
+
257
408
  program
258
409
  .command('dashboard')
259
410
  .description('Show team dashboard — active tasks, ADRs, telemetry summary, health')
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { readGoal, updateGoalFrontmatter, syncIndexEntry, todayIso } from '../lib/goal-store.mjs';
3
+ import { logGoalEvent } from '../lib/goal-events.mjs';
4
+ import { logEvent } from '../lib/telemetry.mjs';
5
+
6
+ export async function goalBumpCommand(goalId, opts = {}) {
7
+ const rootDir = process.cwd();
8
+
9
+ if (!goalId) {
10
+ console.error(chalk.red('✗ Usage: dw goal bump <goal-id> --reason "..."'));
11
+ process.exit(1);
12
+ }
13
+ if (!opts.reason) {
14
+ console.error(chalk.red('✗ --reason "..." required (Q4: explicit reason for material change)'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const goal = readGoal(goalId, rootDir);
19
+ if (!goal) {
20
+ console.error(chalk.red(`✗ Goal ${goalId} not found`));
21
+ process.exit(1);
22
+ }
23
+
24
+ const oldVersion = goal.fm.goal_version || 1;
25
+ const newVersion = oldVersion + 1;
26
+
27
+ updateGoalFrontmatter(goalId, (fm) => {
28
+ fm.goal_version = newVersion;
29
+ fm.last_updated = todayIso();
30
+ return fm;
31
+ }, rootDir);
32
+ syncIndexEntry(goalId, rootDir);
33
+
34
+ logGoalEvent({
35
+ event: 'goal_pivoted',
36
+ goal_id: goalId,
37
+ from_version: oldVersion,
38
+ to_version: newVersion,
39
+ summary: opts.reason,
40
+ changed_by: process.env.USER || process.env.USERNAME || 'unknown',
41
+ }, rootDir);
42
+
43
+ logEvent({ event: 'goal', action: 'bump', name: goalId, from_version: oldVersion, to_version: newVersion }, rootDir);
44
+
45
+ console.log();
46
+ console.log(chalk.green(` ✓ ${chalk.bold(goalId)} version ${oldVersion} → ${newVersion}`));
47
+ console.log(chalk.dim(` Reason: ${opts.reason}`));
48
+ console.log(chalk.dim(` Logged to .dw/events-global.jsonl as goal_pivoted`));
49
+ console.log();
50
+ }
@@ -0,0 +1,120 @@
1
+ import { readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { parseFrontmatter, stringifyFrontmatter } from '../lib/frontmatter.mjs';
5
+ import { readGoal, removeIndexEntry, findLinkedTaskIds, goalDir, todayIso, nowUtc } from '../lib/goal-store.mjs';
6
+ import { logGoalEvent } from '../lib/goal-events.mjs';
7
+ import { logEvent } from '../lib/telemetry.mjs';
8
+
9
+ const TASKS_DIR = '.dw/tasks';
10
+
11
+ function clearParentGoalOnTask(taskId, goalId, rootDir = process.cwd()) {
12
+ const file = join(rootDir, TASKS_DIR, taskId, 'task.md');
13
+ if (!existsSync(file)) return false;
14
+ const content = readFileSync(file, 'utf8');
15
+ const fm = parseFrontmatter(content);
16
+ let changed = false;
17
+ if (fm.parent_goal_id === goalId) { fm.parent_goal_id = 'none'; changed = true; }
18
+ if (Array.isArray(fm.contributing_goal_ids) && fm.contributing_goal_ids.includes(goalId)) {
19
+ fm.contributing_goal_ids = fm.contributing_goal_ids.filter((id) => id !== goalId);
20
+ changed = true;
21
+ }
22
+ if (!changed) return false;
23
+ fm.last_updated = todayIso();
24
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
25
+ writeFileSync(file, stringifyFrontmatter(fm) + body, 'utf8');
26
+ return true;
27
+ }
28
+
29
+ export async function goalDeleteCommand(goalId, opts = {}) {
30
+ const rootDir = process.cwd();
31
+
32
+ if (!goalId) {
33
+ console.error(chalk.red('✗ Usage: dw goal delete <goal-id> [--cascade] [--dry-run] [--hard]'));
34
+ process.exit(1);
35
+ }
36
+
37
+ const goal = readGoal(goalId, rootDir);
38
+ if (!goal) {
39
+ console.error(chalk.red(`✗ Goal ${goalId} not found`));
40
+ process.exit(1);
41
+ }
42
+
43
+ const linked = findLinkedTaskIds(goalId, rootDir);
44
+
45
+ if (opts.dryRun) {
46
+ console.log();
47
+ console.log(chalk.cyan(` Dry-run: delete ${chalk.bold(goalId)}`));
48
+ console.log(chalk.dim(` Mode: ${opts.hard ? 'HARD (remove .dw/goals/' + goalId + '/)' : 'SOFT (status → Abandoned, archived_at set)'}`));
49
+ console.log(chalk.dim(` Linked tasks (${linked.length}): ${linked.join(', ') || '(none)'}`));
50
+ if (opts.cascade && linked.length > 0) {
51
+ console.log(chalk.dim(` Would clear parent_goal_id/contributing_goal_ids on ${linked.length} task(s)`));
52
+ } else if (!opts.cascade && linked.length > 0) {
53
+ console.log(chalk.yellow(` ⚠ ${linked.length} tasks reference this goal — add --cascade or unlink manually first`));
54
+ }
55
+ console.log();
56
+ return;
57
+ }
58
+
59
+ if (linked.length > 0 && !opts.cascade) {
60
+ console.error(chalk.red(`✗ ${linked.length} task(s) link to ${goalId} — re-run with --cascade or unlink manually`));
61
+ console.error(chalk.dim(` Linked: ${linked.join(', ')}`));
62
+ process.exit(1);
63
+ }
64
+
65
+ if (opts.cascade) {
66
+ for (const t of linked) clearParentGoalOnTask(t, goalId, rootDir);
67
+ }
68
+
69
+ if (opts.hard) {
70
+ rmSync(goalDir(goalId, rootDir), { recursive: true, force: true });
71
+ removeIndexEntry(goalId, rootDir);
72
+ logGoalEvent({
73
+ event: 'goal_archived',
74
+ goal_id: goalId,
75
+ archived_at: nowUtc(),
76
+ mode: 'hard',
77
+ cascade_task_count: linked.length,
78
+ }, rootDir);
79
+ logEvent({ event: 'goal', action: 'delete.hard', name: goalId, cascade_count: linked.length }, rootDir);
80
+ console.log();
81
+ console.log(chalk.green(` ✓ Hard-deleted ${chalk.bold(goalId)} (folder removed)`));
82
+ console.log(chalk.dim(` Cleared parent_goal_id on ${linked.length} linked task(s)`));
83
+ console.log();
84
+ return;
85
+ }
86
+
87
+ // Soft delete: status → Abandoned, archived_at set, bump version
88
+ const content = readFileSync(goal.file, 'utf8');
89
+ const fm = parseFrontmatter(content);
90
+ const oldVersion = fm.goal_version || 1;
91
+ fm.status = 'Abandoned';
92
+ fm.archived_at = nowUtc();
93
+ fm.goal_version = oldVersion + 1;
94
+ fm.last_updated = todayIso();
95
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
96
+ writeFileSync(goal.file, stringifyFrontmatter(fm) + body, 'utf8');
97
+
98
+ logGoalEvent({
99
+ event: 'goal_status_changed',
100
+ goal_id: goalId,
101
+ from_status: goal.fm.status,
102
+ to_status: 'Abandoned',
103
+ changed_by: process.env.USER || process.env.USERNAME || 'unknown',
104
+ }, rootDir);
105
+ logGoalEvent({
106
+ event: 'goal_archived',
107
+ goal_id: goalId,
108
+ archived_at: fm.archived_at,
109
+ mode: 'soft',
110
+ cascade_task_count: linked.length,
111
+ }, rootDir);
112
+
113
+ logEvent({ event: 'goal', action: 'delete.soft', name: goalId, cascade_count: linked.length }, rootDir);
114
+
115
+ console.log();
116
+ console.log(chalk.green(` ✓ Soft-deleted ${chalk.bold(goalId)} (status=Abandoned, archived_at=${fm.archived_at})`));
117
+ console.log(chalk.dim(` Cleared parent_goal_id on ${linked.length} linked task(s)`));
118
+ console.log(chalk.dim(` To hard-delete folder: dw goal delete ${goalId} --hard`));
119
+ console.log();
120
+ }