dw-kit 1.9.0-rc.1 → 1.9.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.
@@ -85,6 +85,16 @@ if [ "$ACTIVE_TASK_FORMAT" = "v3" ] && [ -n "$ACTIVE_TASK" ]; then
85
85
  fi
86
86
  fi
87
87
 
88
+ # --- Refresh tasks-index@v1 (ADR-0017 doc contract) — fire-and-forget ---
89
+ # Keeps the adapter read-contract (.dw/tasks/tasks-index.json) fresh after manual
90
+ # task.md edits. Idempotent at the writer (writeTaskIndex sorts keys + skips the
91
+ # write when the task set is byte-identical), so an unchanged set produces ZERO
92
+ # git diff and never blocks a post-commit `git checkout` (#21). Silent; errors ignored.
93
+ DW_BIN="$PROJECT_DIR/bin/dw.mjs"
94
+ if [ -f "$DW_BIN" ] && [ -d "$PROJECT_DIR/.dw/tasks" ] && command -v node >/dev/null 2>&1; then
95
+ node "$DW_BIN" task index >/dev/null 2>&1 || true
96
+ fi
97
+
88
98
  # --- Auto-handoff: append snippet to active task's tracking/timeline file if uncommitted ---
89
99
  if [ "$HAS_UNCOMMITTED" = "1" ] && [ -n "$ACTIVE_TASK_FILE" ]; then
90
100
  TS=$(date -u +"%Y-%m-%d %H:%M UTC")
@@ -73,12 +73,13 @@ Show preview (first 30 lines) rồi ask:
73
73
  Save this ADR to .dw/decisions/{NNNN}-{title}.md? (Y/n)
74
74
  ```
75
75
 
76
- Nếu yes → write file + print:
76
+ Nếu yes → write file, sau đó chạy `dw decision index` (cập nhật `decisions-index@v1` — read-contract cho adapter, ADR-0017 v1.1), rồi print:
77
77
  ```
78
78
  ✓ ADR-{NNNN} created
79
79
  Status: Proposed
80
80
  Next: Review → change status to Accepted when approved
81
81
  Related task: update task.md (v3) / tracking.md (v2) với reference
82
+ decisions-index refreshed (dw decision index)
82
83
  ```
83
84
 
84
85
  ## Quality Bar
@@ -17,6 +17,18 @@ $ARGUMENTS
17
17
 
18
18
  ---
19
19
 
20
+ ## Nguyên tắc riêng tư (ĐỌC TRƯỚC — bắt buộc)
21
+
22
+ Skill này tạo issue trên **repo bên thứ ba `dv-workflow/dv-workflow`** (ngoài project của bạn). Vì vậy **KHÔNG bao giờ tự động đưa thông tin nhận dạng của project người report vào issue**:
23
+
24
+ - ❌ KHÔNG kèm `project.name` từ config (đây là tên codebase/đơn vị của người dùng).
25
+ - ❌ KHÔNG kèm tên task, mã ticket nội bộ (vd `PROJ-123`), đường dẫn file độc quyền, hay code snippet.
26
+ - ✅ CHỈ gửi: loại feedback, component của **dw-kit**, mô tả vấn đề (từ `$ARGUMENTS`), và môi trường tối thiểu (OS, shell, dw version).
27
+
28
+ Nếu chính `$ARGUMENTS` chứa thông tin nhận dạng nội bộ → cảnh báo người dùng và đề nghị lược bỏ. **Luôn preview + xin xác nhận trước khi gửi (Bước 4).**
29
+
30
+ ---
31
+
20
32
  ## Bước 1: Thu Thập Context
21
33
 
22
34
  **OS detection:**
@@ -24,9 +36,9 @@ $ARGUMENTS
24
36
  uname -s 2>/dev/null || echo "Windows"
25
37
  ```
26
38
 
27
- **dw version:** Đọc `_toolkit.core_version` từ `.dw/config/dw.config.yml`
39
+ **dw version:** Đọc `_toolkit.core_version` từ `.dw/config/dw.config.yml` (KHÔNG đọc `project.name`).
28
40
 
29
- **Task context:** Kiểm tra `.dw/tasks/`task nào đang In Progress?
41
+ **Workflow context:** task In Progress không, và depth của nó (quick/standard/thorough)chỉ để giúp reproduce. Ghi nhận CÓ/KHÔNG + depth, **KHÔNG lấy tên task**.
30
42
 
31
43
  ---
32
44
 
@@ -83,8 +95,8 @@ Ví dụ:
83
95
  [Nội dung từ $ARGUMENTS — đầy đủ, rõ ràng]
84
96
 
85
97
  ## Context
86
- - Task khi gặp vấn đề: [task name hoặc "general usage"]
87
- - Command/skill liên quan: [nếu biết]
98
+ - Hoàn cảnh: ["general usage" hoặc "trong một task depth=X" — KHÔNG ghi tên task/ticket]
99
+ - Command/skill dw-kit liên quan: [nếu biết]
88
100
  - Bước reproduce (nếu là bug):
89
101
  1. ...
90
102
  2. ...
@@ -95,12 +107,31 @@ Ví dụ:
95
107
  - [ ] Minor — annoying, có workaround dễ
96
108
 
97
109
  ---
98
- *Reported via `/dw:kit-report` | Project: [project.name từ config]*
110
+ *Reported via `/dw:kit-report`*
99
111
  ```
100
112
 
113
+ > Không thêm dòng `Project:` hay bất kỳ tên project/ticket/path nội bộ nào (xem Nguyên tắc riêng tư).
114
+
115
+ ---
116
+
117
+ ## Bước 4: Preview + Xác Nhận (BẮT BUỘC trước khi gửi)
118
+
119
+ Issue sẽ được tạo trên repo bên thứ ba **công khai với dw-kit team**. Trước khi gửi, hiện đầy đủ title + body sẽ post và CHỜ người dùng đồng ý:
120
+
121
+ ```
122
+ ┌─ Sẽ gửi lên github.com/dv-workflow/dv-workflow ─────────────┐
123
+ TITLE: [title]
124
+
125
+ BODY:
126
+ [body đầy đủ]
127
+ └─ Gõ "ok" để gửi · hoặc sửa nội dung trước ──────────────────┘
128
+ ```
129
+
130
+ Trước khi hiện preview, **rà soát lần cuối**: nếu title/body (kể cả phần người dùng tự nhập trong `$ARGUMENTS`) còn chứa tên project, mã ticket nội bộ, đường dẫn file độc quyền, hay code snippet → cảnh báo rõ và đề nghị lược bỏ. **Chỉ sang Bước 5 sau khi người dùng xác nhận.**
131
+
101
132
  ---
102
133
 
103
- ## Bước 4: Gửi Lên GitHub
134
+ ## Bước 5: Gửi Lên GitHub
104
135
 
105
136
  **Kiểm tra `gh` CLI:**
106
137
  ```bash
@@ -141,7 +172,7 @@ In ra:
141
172
 
142
173
  ---
143
174
 
144
- ## Bước 5: Xác Nhận
175
+ ## Bước 6: Xác Nhận
145
176
 
146
177
  ```
147
178
  ✓ Issue đã được gửi: https://github.com/dv-workflow/dv-workflow/issues/[N]
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/dv-workflow/dv-workflow/blob/main/.dw/core/schemas/decision-frontmatter.schema.json",
4
+ "title": "decision-frontmatter",
5
+ "description": "Normalized ADR metadata for decisions-index@v1 (ADR-0017 v1.1). ADRs use two on-disk styles: YAML frontmatter (early, e.g. ADR-0001) OR markdown headers `# ADR-NNNN: Title` + `## Status:`/`## Date:`/`## Deciders:`/`## Related:`. The `dw decision index` parser tolerates both and projects to this normalized shape. New ADRs MAY adopt YAML frontmatter with these keys for lossless extraction.",
6
+ "type": "object",
7
+ "required": ["title", "status"],
8
+ "additionalProperties": true,
9
+ "properties": {
10
+ "title": {
11
+ "type": "string",
12
+ "description": "ADR title (without the `ADR-NNNN:` prefix)"
13
+ },
14
+ "status": {
15
+ "type": "string",
16
+ "enum": ["Proposed", "Accepted", "Deprecated", "Superseded"],
17
+ "description": "Normalized lifecycle status keyword (extracted from a possibly-decorated status line, e.g. 'Accepted (Round 2, ...)')"
18
+ },
19
+ "status_raw": {
20
+ "type": ["string", "null"],
21
+ "description": "Original status string before normalization"
22
+ },
23
+ "date": {
24
+ "type": ["string", "null"],
25
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
26
+ "description": "First ISO date found in the Date field/header (YYYY-MM-DD)"
27
+ },
28
+ "deciders": {
29
+ "type": ["string", "null"],
30
+ "description": "Free-form deciders (names/roles)"
31
+ },
32
+ "related": {
33
+ "type": "array",
34
+ "items": { "type": "string", "pattern": "^ADR-\\d{3,4}$" },
35
+ "uniqueItems": true,
36
+ "description": "Referenced ADRs (from Related field/header + status line); excludes self. Causal-link candidates for org-memory ingest."
37
+ },
38
+ "supersedes": {
39
+ "type": "array",
40
+ "items": { "type": "string", "pattern": "^ADR-\\d{3,4}$" },
41
+ "uniqueItems": true,
42
+ "description": "ADRs this one supersedes"
43
+ },
44
+ "superseded_by": {
45
+ "type": ["string", "null"],
46
+ "pattern": "^ADR-\\d{3,4}$",
47
+ "description": "ADR that supersedes this one (from 'Superseded by ADR-NNNN' or frontmatter)"
48
+ },
49
+ "file": {
50
+ "type": "string",
51
+ "description": "Repo-relative path to the ADR markdown file"
52
+ }
53
+ }
54
+ }
@@ -73,12 +73,12 @@
73
73
  "icon": {
74
74
  "type": ["string", "null"],
75
75
  "maxLength": 8,
76
- "description": "Optional emoji/icon for visual differentiation in portfolio cards (bigokr borrow)."
76
+ "description": "Optional emoji/icon for visual differentiation in portfolio cards."
77
77
  },
78
78
  "cycle": {
79
79
  "type": ["string", "null"],
80
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."
81
+ "description": "Optional temporal grouping label e.g. 'Q1 2026', 'v1.7-cycle', '2026 H1'. Portfolio groups goals by cycle."
82
82
  }
83
83
  }
84
84
  }
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "schema_version": {
69
69
  "type": "string",
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."
70
+ "enum": ["task@v3.0", "task@v3.1", "v3.0", "v3.1"],
71
+ "description": "Frontmatter schema version. Canonical form is namespaced (task@v3.0/task@v3.1) to match the repo-wide @v convention (goal@v1, tasks-index@v1); bare v3.0/v3.1 stay valid for back-compat. v3.1 adds optional parent_goal_id, contributing_goal_ids, summary per ADR-0010 (#22)."
72
72
  },
73
73
  "blockers": {
74
74
  "type": "string",
@@ -8,7 +8,7 @@ owner: {name}
8
8
  depth: quick | standard | thorough
9
9
  related_adr: {ADR-NNNN | none}
10
10
  target_ship: {milestone or TBD}
11
- schema_version: v3.0
11
+ schema_version: task@v3.0
12
12
  blockers: none
13
13
  ---
14
14
 
@@ -66,25 +66,42 @@ genuinely changes.
66
66
 
67
67
  {Forcing function: deadline, incident, dependency, opportunity.}
68
68
 
69
- ### Subtasks (in scope)
70
-
71
- **ST-1: {Subtask name}**
72
- - {Concrete action}
73
- - Acceptance: {verifiable criterion}
74
-
75
- **ST-2: {Subtask name}**
76
- - ...
69
+ <!--
70
+ Subtasks live in the Section 3 tracker (one list, with per-subtask Acceptance +
71
+ Est columns) — NOT duplicated here. Section 2 stays the stable intent contract;
72
+ Section 3 owns the subtask breakdown + status.
73
+ -->
77
74
 
78
75
  ### Out of Scope
79
76
 
80
77
  - {Explicit exclusions — prevents scope creep}
81
78
 
82
- ### Success Criteria
79
+ ### Functional Acceptance (handoff scenarios)
83
80
 
84
- Measurable outcomes (not vibes):
81
+ <!--
82
+ Feature behaviour in business / user language — readable by PM/BA/QC WITHOUT code
83
+ (no API/file/variable names). This is the cross-role handoff contract, NOT a full
84
+ test plan (`/dw:test-plan` expands it into QC cases). Per-component acceptance
85
+ lives with each subtask; this is the feature-level scenario layer.
86
+ -->
85
87
 
86
- - [ ] {Numeric threshold, e.g., "p95 latency <100ms"}
87
- - [ ] {Binary gate, e.g., "passes smoke test"}
88
+ - [ ] {User-visible scenario, e.g., "User resets password and gets a confirmation email"}
89
+ - [ ] {Error / edge scenario, e.g., "Expired reset link shows a clear retry prompt"}
90
+ - [ ] {Measurable gate where relevant, e.g., "p95 latency <100ms"}
91
+
92
+ ### Definition of Done (role lens)
93
+
94
+ <!--
95
+ Plain-language sign-off anchored to the outcome. Roles absent from `team.roles`
96
+ sign by proxy / degrade gracefully. These are `- [ ]` checkboxes (NOT status
97
+ markers — those live only in Section 3).
98
+ -->
99
+
100
+ - [ ] 🎯 **Goal** — result advances the linked goal's KR (update the goal)
101
+ - [ ] 📋 **BA** — matches the business expectation
102
+ - [ ] ✅ **QC** — handoff scenarios pass, incl. error cases + no regression
103
+ - [ ] 🛠 **TL** — approach/code sound, no new tech debt, automated checks pass
104
+ - [ ] 🤝 **Handoff** — docs sufficient + changes merged
88
105
 
89
106
  ### Dependencies
90
107
 
@@ -100,14 +117,19 @@ Measurable outcomes (not vibes):
100
117
  ## 3. Subtask Tracker
101
118
 
102
119
  <!--
103
- SINGLE SOURCE OF TRUTH for subtask status. This is the only section where
104
- status markers (⬜🟡✅🔴⏸) are allowed.
120
+ SINGLE SOURCE OF TRUTH for the subtask breakdown AND status. The subtask list is
121
+ NOT duplicated in Section 2 (#24). This is the only section where status markers
122
+ (⬜🟡✅🔴⏸) are allowed — Section 2 stays status-free (drift-prevention).
123
+
124
+ Columns (read by name, order-tolerant): Acceptance = per-subtask verifiable
125
+ criterion; Est = optional effort estimate (hours / story-points / t-shirt per
126
+ `estimation_unit`, `—` if unused, see `/dw:estimate`).
105
127
  -->
106
128
 
107
- | # | Subtask | Status | Date | Notes |
108
- |---|---------|--------|------|-------|
109
- | ST-1 | ... | Pending | — | |
110
- | ST-2 | ... | ⬜ Pending | | |
129
+ | # | Subtask | Acceptance | Est | Status | Notes |
130
+ |---|---------|-----------|-----|--------|-------|
131
+ | ST-1 | {name} | {verifiable criterion} | — | ⬜ Pending | |
132
+ | ST-2 | {name} | ... | — | ⬜ Pending | |
111
133
 
112
134
  Status legend: ⬜ Pending · 🟡 In Progress · ✅ Done · 🔴 Blocked · ⏸ Paused
113
135
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > An AI development workflow toolkit for teams using agentic IDEs (Claude Code, Cursor) — from idea to review-ready commits.
4
4
 
5
- **v1.3.6** · `npm install -g dw-kit` · [Docs](docs/README.md) · [Get started](docs/get-started.md) · [Cheatsheet](docs/cheatsheet.md) · [Migration v1.3](MIGRATION-v1.3.md) · [Changelog](CHANGELOG.md)
5
+ **v1.9.1** · `npm install -g dw-kit` · [Docs](docs/README.md) · [Get started](docs/get-started.md) · [Cheatsheet](docs/cheatsheet.md) · [Migration v1.3](MIGRATION-v1.3.md) · [Changelog](CHANGELOG.md)
6
6
 
7
7
  ---
8
8
 
@@ -36,7 +36,10 @@ It’s designed for collaboration (Dev / Tech Lead / QA / PM) and keeps work aud
36
36
 
37
37
  ## Release notes
38
38
 
39
- - **v1.6.0-rc.1 (current)** — **Agent OS Multi-Agent Orchestration** ([ADR-0009](.dw/decisions/0009-agent-os-multi-agent-orchestration.md)): file-based cooperative protocol for Claude Code / Codex / Gemini / human agents to work from same `task.md` without conflict. 6-command CLI `dw agent *` (claim / release / expire / claims / reports / conflicts). Dual ownership model: `subtasks` (semantic) + `write_scope` (filesystem). Lifecycle states + lease (wall+relative) per Round 2. Audit trail via committed `reports/` + `events.jsonl`; claims gitignored ephemeral. Trust = cooperative, post-hoc conflict detection.
39
+ - **v1.9.0 (current, stable)** — substrate-complete for the realtime voice-orchestration Root Goal: persistent CLI-agent sessions, Telegram bridge, multi-workspace registry, browser voice MVP + orchestrator action-execution, multi-agent live debate, and the **DW Event/Document schema + adapter protocol** ([ADR-0011](.dw/decisions/0011-session-runtime-voice-orchestrator.md)/[0014](.dw/decisions/0014-orchestrator-action-execution.md)/[0015](.dw/decisions/0015-multi-agent-live-debate.md)/[0016](.dw/decisions/0016-dw-goal-outcome-pursuit-loop.md)/[0017](.dw/decisions/0017-dw-document-schema-index.md)). Plus team-dogfood fixes: `dw goal status` (#20), deterministic index writers (#21), `task@v3.x` schema_version converge (#22), v3 template Functional Acceptance + role-lens DoD + Est (#24).
40
+ - **v1.8.0** — remote-agent-first substrate: `dw session *`, `dw connector telegram`, `dw workspace *`, `dw voice` (bilingual en+vi, hybrid orchestrator, action-execution with voice confirm).
41
+ - **v1.7.0** — **Goals Layer (OKR-inspired)** ([ADR-0010](.dw/decisions/0010-goals-management-layer.md)): strategic layer above tasks — `dw goal *`, `goals-index@v1`, bidirectional task↔goal linking, portfolio + HTML view.
42
+ - **v1.6.0** — **Agent OS Multi-Agent Orchestration** ([ADR-0009](.dw/decisions/0009-agent-os-multi-agent-orchestration.md)): file-based cooperative protocol for Claude Code / Codex / Gemini / human agents to work from same `task.md` without conflict. 6-command CLI `dw agent *` (claim / release / expire / claims / reports / conflicts). Dual ownership model: `subtasks` (semantic) + `write_scope` (filesystem). Audit trail via committed `reports/` + `events.jsonl`; claims gitignored ephemeral.
40
43
  - **v1.5.0 — Task Docs v3** ([ADR-0008](.dw/decisions/0008-task-docs-format-one-file-timeline.md)): single-file `task.md` replaces v2 `spec.md` + `tracking.md` (drift fixed structurally). 8-command CLI `dw task *` (show / new / view / watch / render / lint / migrate / rotate). Lean HTML dashboard for human dev orchestrator (~10KB, scan-in-5-seconds) + bounded SVG sidecar via `dw-kit-render` v0.2. Live preview server with auto-refresh. Migration script with `--dry-run`, `--diff`, `--rollback`. Lint strict-default blocks drift markers. See [`MIGRATION-v1.5.md`](MIGRATION-v1.5.md).
41
44
  - **v1.4 (in progress)** — Optional **Review Render Pipeline** ([ADR-0007](.dw/decisions/0007-decoupled-review-render-pipeline.md)): `/dw:review --visual` plus a separate `dw-kit-render` package turn findings into SVG + PNG cards for PR comments / Slack / stakeholders. Pure JS + WASM, universal `npm install`, no system deps. See [`docs/review-renderer.md`](docs/review-renderer.md).
42
45
  - **v1.3.6** (2026-05-14) — Supply-Chain Guard upgraded to 3-pillar architecture: OSV snapshot + curated IoC fixture (version-aware, wired into default scan) + **AI-Native NEW-package heuristic** that catches zero-day-ish risk at the AI-edit boundary. See [`CHANGELOG.md#v136--2026-05-14`](CHANGELOG.md#v136--2026-05-14) and [ADR-0006](.dw/decisions/0006-supply-chain-guard-heuristic.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dw-kit",
3
- "version": "1.9.0-rc.1",
3
+ "version": "1.9.1",
4
4
  "description": "AI development workflow toolkit — structured, quality-assured, team-ready. From requirements to dashboard.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "src/commands/",
13
13
  "src/lib/",
14
14
  ".dw/core/",
15
+ "!.dw/core/PILLARS.md",
15
16
  ".dw/config/",
16
17
  ".dw/adapters/",
17
18
  ".dw/security/",
@@ -50,7 +51,6 @@
50
51
  ".claude/skills/dw-upgrade/",
51
52
  ".claude/templates/",
52
53
  ".claude/settings.json",
53
- "CLAUDE.md",
54
54
  "MIGRATION-v1.3.md",
55
55
  "MIGRATION-v1.5.md",
56
56
  "NOTICE",
@@ -65,7 +65,9 @@
65
65
  "test:renderer": "cd packages/dw-kit-render && npm test",
66
66
  "link": "npm link",
67
67
  "test:e2e-local": "bash scripts/e2e-local-check.sh",
68
- "gen:event-schemas": "node scripts/generate-event-schemas.mjs"
68
+ "gen:event-schemas": "node scripts/generate-event-schemas.mjs",
69
+ "audit:pack": "node scripts/audit-pack.mjs",
70
+ "prepublishOnly": "node scripts/audit-pack.mjs"
69
71
  },
70
72
  "keywords": [
71
73
  "ai",
package/src/cli.mjs CHANGED
@@ -195,6 +195,30 @@ export function run(argv) {
195
195
  await taskSummaryCommand(taskName, opts);
196
196
  });
197
197
 
198
+ taskCmd
199
+ .command('index')
200
+ .description('Regenerate .dw/tasks/tasks-index.json — DW Document Schema + Index v1.0 read-contract (ADR-0017)')
201
+ .option('--json', 'Print the machine-readable index (for external adapters)')
202
+ .option('--check', 'Print the current index without rebuilding')
203
+ .action(async (opts) => {
204
+ const { taskIndexCommand } = await import('./commands/task-index.mjs');
205
+ await taskIndexCommand(opts);
206
+ });
207
+
208
+ const decisionCmd = program
209
+ .command('decision')
210
+ .description('ADR operations (ADR-0017 v1.1 document contract)');
211
+
212
+ decisionCmd
213
+ .command('index')
214
+ .description('Regenerate .dw/decisions/decisions-index.json — decisions-index@v1 read-contract (ADR-0017 v1.1; consumed by external org-memory adapters)')
215
+ .option('--json', 'Print the machine-readable index (for adapters)')
216
+ .option('--check', 'Print the current index without rebuilding')
217
+ .action(async (opts) => {
218
+ const { decisionIndexCommand } = await import('./commands/decision-index.mjs');
219
+ await decisionIndexCommand(opts);
220
+ });
221
+
198
222
  const agentCmd = program
199
223
  .command('agent')
200
224
  .description('Agent OS multi-agent orchestration (ADR-0009): claim · release · renew · expire · claims · reports · conflicts · check-staged · verify');
@@ -495,6 +519,15 @@ export function run(argv) {
495
519
  await goalSetCommand(goalId, opts);
496
520
  });
497
521
 
522
+ goalCmd
523
+ .command('status <goal-id> <new-status>')
524
+ .description('Transition lifecycle status (Draft|Active|Achieved|Pivoted); auto-bumps goal_version + emits goal_status_changed. Use `goal delete` to abandon.')
525
+ .option('--reason <text>', 'Optional reason, recorded in the goal_status_changed event')
526
+ .action(async (goalId, newStatus, opts) => {
527
+ const { goalStatusCommand } = await import('./commands/goal-status.mjs');
528
+ await goalStatusCommand(goalId, newStatus, opts);
529
+ });
530
+
498
531
  goalCmd
499
532
  .command('show [goal-id]')
500
533
  .description('ANSI snapshot of a goal (no arg = list all)')
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+ import { rebuildDecisionIndex, readDecisionIndex, decisionIndexFile } from '../lib/decision-store.mjs';
3
+ import { logEvent } from '../lib/telemetry.mjs';
4
+
5
+ // `dw decision index` — regenerate .dw/decisions/decisions-index.json
6
+ // (DW Document Schema + Index v1.0, ADR-0017 v1.1). Read-contract surface for
7
+ // external org-memory adapters.
8
+ // dw decision index rebuild + human summary
9
+ // dw decision index --json rebuild + print machine-readable index
10
+ // dw decision index --check print current index without rebuilding
11
+ export async function decisionIndexCommand(opts = {}) {
12
+ const rootDir = process.cwd();
13
+ const index = opts.check ? readDecisionIndex(rootDir) : rebuildDecisionIndex(rootDir);
14
+
15
+ if (opts.json) {
16
+ console.log(JSON.stringify(index, null, 2));
17
+ logEvent({ event: 'decision', action: 'index.json', name: Object.keys(index.decisions).length }, rootDir);
18
+ return;
19
+ }
20
+
21
+ const decisions = index.decisions || {};
22
+ const ids = Object.keys(decisions);
23
+ const byStatus = {};
24
+ for (const id of ids) {
25
+ const s = decisions[id].status || 'Proposed';
26
+ byStatus[s] = (byStatus[s] || 0) + 1;
27
+ }
28
+
29
+ console.log();
30
+ console.log(chalk.bold(` decisions-index (${chalk.cyan(index.schema_version)}) — ${ids.length} ADR(s)`));
31
+ console.log(chalk.dim(` ${decisionIndexFile(rootDir)}`));
32
+ console.log();
33
+ if (ids.length === 0) {
34
+ console.log(chalk.dim(' (no ADRs — create one with /dw:decision)'));
35
+ } else {
36
+ for (const [status, n] of Object.entries(byStatus)) {
37
+ console.log(` ${String(n).padStart(3)} ${status}`);
38
+ }
39
+ }
40
+ console.log();
41
+ console.log(chalk.dim(' Tip: `dw decision index --json` for the machine-readable adapter contract.'));
42
+ console.log();
43
+
44
+ logEvent({ event: 'decision', action: opts.check ? 'index.check' : 'index.rebuild', name: ids.length }, rootDir);
45
+ }
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { parseFrontmatter, stringifyFrontmatter } from '../lib/frontmatter.mjs';
5
- import { readGoal, removeIndexEntry, findLinkedTaskIds, goalDir, todayIso, nowUtc } from '../lib/goal-store.mjs';
5
+ import { readGoal, removeIndexEntry, syncIndexEntry, findLinkedTaskIds, goalDir, todayIso, nowUtc } from '../lib/goal-store.mjs';
6
6
  import { logGoalEvent } from '../lib/goal-events.mjs';
7
7
  import { logEvent } from '../lib/telemetry.mjs';
8
8
 
@@ -94,6 +94,8 @@ export async function goalDeleteCommand(goalId, opts = {}) {
94
94
  fm.last_updated = todayIso();
95
95
  const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
96
96
  writeFileSync(goal.file, stringifyFrontmatter(fm) + body, 'utf8');
97
+ // Keep goals-index in sync — otherwise `goal lint` flags status drift (#20).
98
+ syncIndexEntry(goalId, rootDir);
97
99
 
98
100
  logGoalEvent({
99
101
  event: 'goal_status_changed',
@@ -5,6 +5,7 @@ import { parseFrontmatter, stringifyFrontmatter } from '../lib/frontmatter.mjs';
5
5
  import { readGoal, syncIndexEntry, todayIso } from '../lib/goal-store.mjs';
6
6
  import { logGoalEvent } from '../lib/goal-events.mjs';
7
7
  import { logEvent } from '../lib/telemetry.mjs';
8
+ import { bareSchemaVersion } from '../lib/task-store.mjs';
8
9
 
9
10
  const TASKS_DIR = '.dw/tasks';
10
11
 
@@ -17,7 +18,8 @@ function writeTaskFrontmatter(taskId, mutator, rootDir = process.cwd()) {
17
18
  const content = readFileSync(file, 'utf8');
18
19
  const fm = parseFrontmatter(content);
19
20
  const updated = mutator({ ...fm });
20
- if (updated.schema_version === 'v3.0') updated.schema_version = 'v3.1';
21
+ // Linking goal fields requires v3.1; bump v3.0 canonical task@v3.1 (#22).
22
+ if (bareSchemaVersion(updated.schema_version) === 'v3.0') updated.schema_version = 'task@v3.1';
21
23
  updated.last_updated = todayIso();
22
24
  const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
23
25
  writeFileSync(file, stringifyFrontmatter(updated) + body, 'utf8');
@@ -0,0 +1,95 @@
1
+ import chalk from 'chalk';
2
+ import {
3
+ readGoal,
4
+ updateGoalFrontmatter,
5
+ syncIndexEntry,
6
+ goalStatusEnum,
7
+ todayIso,
8
+ } from '../lib/goal-store.mjs';
9
+ import { logGoalEvent } from '../lib/goal-events.mjs';
10
+ import { logEvent } from '../lib/telemetry.mjs';
11
+
12
+ // Statuses whose entry carries cascade + archive semantics owned by `goal delete`.
13
+ const DELEGATED_STATUSES = new Set(['Abandoned']);
14
+ // Closed states — reopening them is a backward move worth warning about.
15
+ const CLOSED_STATUSES = new Set(['Achieved', 'Abandoned']);
16
+
17
+ // Title-case a user-typed status so `active` matches the PascalCase enum (#20).
18
+ function normalizeStatus(input, allowed) {
19
+ if (!input) return input;
20
+ const exact = allowed.find((s) => s === input);
21
+ if (exact) return exact;
22
+ return allowed.find((s) => s.toLowerCase() === input.toLowerCase()) || input;
23
+ }
24
+
25
+ export async function goalStatusCommand(goalId, newStatusRaw, opts = {}) {
26
+ const rootDir = process.cwd();
27
+
28
+ const allowed = goalStatusEnum(rootDir);
29
+ if (!goalId || !newStatusRaw) {
30
+ console.error(chalk.red(`✗ Usage: dw goal status <goal-id> <${allowed.join('|')}> [--reason "..."]`));
31
+ process.exit(1);
32
+ }
33
+
34
+ const newStatus = normalizeStatus(newStatusRaw, allowed);
35
+ if (!allowed.includes(newStatus)) {
36
+ console.error(chalk.red(`✗ Invalid status "${newStatusRaw}". Allowed: ${allowed.join(', ')}`));
37
+ process.exit(1);
38
+ }
39
+
40
+ // Abandonment is owned by `goal delete` (linked-task cascade + goal_archived
41
+ // event). Redirect rather than create a second, divergent abandon path (#20).
42
+ if (DELEGATED_STATUSES.has(newStatus)) {
43
+ console.error(chalk.red(`✗ Use \`dw goal delete ${goalId}\` to abandon a goal.`));
44
+ console.error(chalk.dim(' It handles linked-task cleanup (--cascade) and emits goal_archived.'));
45
+ process.exit(1);
46
+ }
47
+
48
+ const goal = readGoal(goalId, rootDir);
49
+ if (!goal) {
50
+ console.error(chalk.red(`✗ Goal ${goalId} not found`));
51
+ process.exit(1);
52
+ }
53
+
54
+ const fromStatus = goal.fm.status || 'Draft';
55
+ if (fromStatus === newStatus) {
56
+ console.log(chalk.dim(` ${goalId} is already ${newStatus} — no change.`));
57
+ return;
58
+ }
59
+
60
+ // Warn (don't block) on reopening a closed goal — least surprise per ADR-0001.
61
+ if (CLOSED_STATUSES.has(fromStatus)) {
62
+ console.error(chalk.yellow(` ⚠ reopening a ${fromStatus} goal → ${newStatus}`));
63
+ }
64
+
65
+ const changedBy = process.env.USER || process.env.USERNAME || 'unknown';
66
+ const oldVersion = goal.fm.goal_version || 1;
67
+ const newVersion = oldVersion + 1; // ADR-0010 Q4/C-1: status transitions are material → auto-bump
68
+
69
+ updateGoalFrontmatter(goalId, (fm) => {
70
+ fm.status = newStatus;
71
+ fm.goal_version = newVersion;
72
+ // Leaving any non-Abandoned target must clear a stale archive timestamp
73
+ // (schema: archived_at null = active). Covers reopen from a soft-delete.
74
+ if (fm.archived_at) fm.archived_at = null;
75
+ fm.last_updated = todayIso();
76
+ return fm;
77
+ }, rootDir);
78
+ syncIndexEntry(goalId, rootDir);
79
+
80
+ logGoalEvent({
81
+ event: 'goal_status_changed',
82
+ goal_id: goalId,
83
+ from_status: fromStatus,
84
+ to_status: newStatus,
85
+ changed_by: changedBy,
86
+ ...(opts.reason ? { reason: opts.reason } : {}),
87
+ }, rootDir);
88
+ logEvent({ event: 'goal', action: 'status', name: goalId, from: fromStatus, to: newStatus }, rootDir);
89
+
90
+ console.log();
91
+ console.log(chalk.green(` ✓ ${chalk.bold(goalId)} status ${fromStatus} → ${newStatus} (v${oldVersion} → v${newVersion})`));
92
+ if (opts.reason) console.log(chalk.dim(` Reason: ${opts.reason}`));
93
+ console.log(chalk.dim(' Logged to .dw/events-global.jsonl as goal_status_changed'));
94
+ console.log();
95
+ }
@@ -106,6 +106,26 @@ export async function lintTaskCommand(taskName, opts = {}) {
106
106
  level,
107
107
  }, rootDir);
108
108
 
109
+ // tasks-index freshness advisory (ADR-0017 doc contract) — read-only, never mutates.
110
+ try {
111
+ const { readTaskIndex, listTaskIds, readTask } = await import('../lib/task-store.mjs');
112
+ const index = readTaskIndex(rootDir);
113
+ const idxKeys = new Set(Object.keys(index.tasks || {}));
114
+ const onDisk = listTaskIds(rootDir);
115
+ const missing = onDisk.filter((id) => !idxKeys.has(id));
116
+ let statusDrift = 0;
117
+ for (const id of onDisk) {
118
+ const entry = index.tasks?.[id];
119
+ if (!entry) continue;
120
+ const t = readTask(id, rootDir);
121
+ if (t && (t.fm.status || 'Draft') !== entry.status) statusDrift++;
122
+ }
123
+ if (missing.length || statusDrift) {
124
+ console.log(chalk.dim(` ℹ tasks-index stale (${missing.length} unindexed, ${statusDrift} status drift) — run \`dw task index\``));
125
+ console.log();
126
+ }
127
+ } catch { /* advisory only */ }
128
+
109
129
  if (level === 'strict' && totalErrors > 0) {
110
130
  process.exit(1);
111
131
  }
@@ -0,0 +1,47 @@
1
+ import chalk from 'chalk';
2
+ import { rebuildTaskIndex, readTaskIndex, taskIndexFile } from '../lib/task-store.mjs';
3
+ import { logEvent } from '../lib/telemetry.mjs';
4
+
5
+ // `dw task index` — regenerate .dw/tasks/tasks-index.json (DW Document Schema
6
+ // + Index v1.0, ADR-0017). Read-only contract surface for adapters.
7
+ // dw task index rebuild + human summary
8
+ // dw task index --json rebuild + print machine-readable index
9
+ // dw task index --check print current index without rebuilding (--json optional)
10
+ export async function taskIndexCommand(opts = {}) {
11
+ const rootDir = process.cwd();
12
+ const index = opts.check ? readTaskIndex(rootDir) : rebuildTaskIndex(rootDir);
13
+
14
+ if (opts.json) {
15
+ console.log(JSON.stringify(index, null, 2));
16
+ logEvent({ event: 'task', action: 'index.json', name: Object.keys(index.tasks).length }, rootDir);
17
+ return;
18
+ }
19
+
20
+ const tasks = index.tasks || {};
21
+ const ids = Object.keys(tasks);
22
+ const byStatus = {};
23
+ for (const id of ids) {
24
+ const s = tasks[id].status || 'Draft';
25
+ byStatus[s] = (byStatus[s] || 0) + 1;
26
+ }
27
+
28
+ console.log();
29
+ console.log(chalk.bold(` tasks-index (${chalk.cyan(index.schema_version)}) — ${ids.length} task(s)`));
30
+ console.log(chalk.dim(` ${taskIndexFile(rootDir)}`));
31
+ console.log();
32
+ if (ids.length === 0) {
33
+ console.log(chalk.dim(' (no tasks — scaffold one with `dw task new <name>`)'));
34
+ } else {
35
+ for (const [status, n] of Object.entries(byStatus)) {
36
+ console.log(` ${String(n).padStart(3)} ${status}`);
37
+ }
38
+ const linked = ids.filter((id) => tasks[id].parent_goal_id).length;
39
+ console.log();
40
+ console.log(chalk.dim(` ${linked}/${ids.length} linked to a parent goal`));
41
+ }
42
+ console.log();
43
+ console.log(chalk.dim(' Tip: `dw task index --json` for the machine-readable adapter contract.'));
44
+ console.log();
45
+
46
+ logEvent({ event: 'task', action: opts.check ? 'index.check' : 'index.rebuild', name: ids.length }, rootDir);
47
+ }