dw-kit 1.8.0-rc.2 → 1.9.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 (79) hide show
  1. package/.claude/hooks/stop-check.sh +10 -0
  2. package/.claude/rules/dw.md +2 -0
  3. package/.claude/skills/dw-decision/SKILL.md +2 -1
  4. package/.claude/skills/dw-goal/SKILL.md +206 -0
  5. package/.claude/skills/dw-goal-sync/SKILL.md +131 -0
  6. package/.claude/templates/agent-report.md +35 -35
  7. package/.dw/config/agents.yml +8 -0
  8. package/.dw/core/AGENTS.md +53 -53
  9. package/.dw/core/schemas/decision-frontmatter.schema.json +54 -0
  10. package/.dw/core/schemas/events/created.schema.json +33 -0
  11. package/.dw/core/schemas/events/debate_agent_failed.schema.json +42 -0
  12. package/.dw/core/schemas/events/debate_agent_replied.schema.json +44 -0
  13. package/.dw/core/schemas/events/debate_agent_started.schema.json +37 -0
  14. package/.dw/core/schemas/events/debate_completed.schema.json +36 -0
  15. package/.dw/core/schemas/events/debate_started.schema.json +47 -0
  16. package/.dw/core/schemas/events/goal_archived.schema.json +32 -0
  17. package/.dw/core/schemas/events/goal_created.schema.json +32 -0
  18. package/.dw/core/schemas/events/goal_field_updated.schema.json +35 -0
  19. package/.dw/core/schemas/events/goal_pivoted.schema.json +36 -0
  20. package/.dw/core/schemas/events/goal_status_changed.schema.json +40 -0
  21. package/.dw/core/schemas/events/goal_task_linked.schema.json +33 -0
  22. package/.dw/core/schemas/events/goal_task_unlinked.schema.json +33 -0
  23. package/.dw/core/schemas/events/index.json +185 -0
  24. package/.dw/core/schemas/events/orchestrator_cancelled.schema.json +29 -0
  25. package/.dw/core/schemas/events/orchestrator_completed.schema.json +38 -0
  26. package/.dw/core/schemas/events/orchestrator_confirm.schema.json +33 -0
  27. package/.dw/core/schemas/events/orchestrator_confirmed.schema.json +33 -0
  28. package/.dw/core/schemas/events/orchestrator_error.schema.json +29 -0
  29. package/.dw/core/schemas/events/orchestrator_pending_dropped.schema.json +29 -0
  30. package/.dw/core/schemas/events/orchestrator_pending_expired.schema.json +32 -0
  31. package/.dw/core/schemas/events/orchestrator_recommend_rejected.schema.json +37 -0
  32. package/.dw/core/schemas/events/orchestrator_recommended.schema.json +33 -0
  33. package/.dw/core/schemas/events/orchestrator_spawn_failed.schema.json +29 -0
  34. package/.dw/core/schemas/events/orchestrator_started.schema.json +33 -0
  35. package/.dw/core/schemas/events/orchestrator_timeout.schema.json +29 -0
  36. package/.dw/core/schemas/events/reconciled.schema.json +29 -0
  37. package/.dw/core/schemas/events/reconciled_stale.schema.json +29 -0
  38. package/.dw/core/schemas/events/session.created.schema.json +39 -0
  39. package/.dw/core/schemas/events/session.reconciled.schema.json +33 -0
  40. package/.dw/core/schemas/events/session.status_changed.schema.json +42 -0
  41. package/.dw/core/schemas/events/spawn_failed.schema.json +29 -0
  42. package/.dw/core/schemas/events/started.schema.json +59 -0
  43. package/.dw/core/schemas/events/stopped.schema.json +33 -0
  44. package/.dw/core/schemas/goal-frontmatter.schema.json +2 -2
  45. package/.dw/core/schemas/task-frontmatter.schema.json +2 -2
  46. package/.dw/core/templates/v3/task.md +38 -9
  47. package/.dw/security/advisory-snapshot.json +157 -0
  48. package/LICENSE +201 -21
  49. package/NOTICE +26 -0
  50. package/README.md +5 -2
  51. package/SECURITY.md +87 -0
  52. package/TRADEMARK.md +65 -0
  53. package/bin/dw.mjs +1 -1
  54. package/package.json +13 -5
  55. package/src/cli.mjs +33 -0
  56. package/src/commands/decision-index.mjs +45 -0
  57. package/src/commands/goal-delete.mjs +3 -1
  58. package/src/commands/goal-link.mjs +3 -1
  59. package/src/commands/goal-status.mjs +95 -0
  60. package/src/commands/lint-task.mjs +20 -0
  61. package/src/commands/task-index.mjs +47 -0
  62. package/src/commands/task-migrate.mjs +16 -5
  63. package/src/commands/task-new.mjs +6 -0
  64. package/src/commands/task-summary.mjs +4 -3
  65. package/src/commands/voice.mjs +590 -4
  66. package/src/lib/board-data.mjs +220 -0
  67. package/src/lib/debate.mjs +325 -0
  68. package/src/lib/decision-store.mjs +146 -0
  69. package/src/lib/event-schema.mjs +342 -0
  70. package/src/lib/goal-store.mjs +40 -1
  71. package/src/lib/lint-rules.mjs +10 -1
  72. package/src/lib/orchestrator.mjs +31 -9
  73. package/src/lib/session-store.mjs +36 -4
  74. package/src/lib/task-store.mjs +164 -0
  75. package/src/lib/voice-action.mjs +165 -0
  76. package/src/lib/voice-parser.mjs +13 -0
  77. package/.dw/config/connectors.local.yml +0 -38
  78. package/.dw/core/PILLARS.md +0 -122
  79. package/CLAUDE.md +0 -44
package/TRADEMARK.md ADDED
@@ -0,0 +1,65 @@
1
+ # dw-kit Trademark Policy
2
+
3
+ ## Marks covered
4
+
5
+ The following names and logos (the "Marks") are trademarks of the dw-kit copyright holder (currently Đặng Huy / huygdv):
6
+
7
+ - `dw`
8
+ - `dw-kit`
9
+ - `dw Platform`
10
+ - `DW Cloud`, `DW GoalOps`, `DW Memory` (reserved for future product layers)
11
+ - Any logo, wordmark, or icon distributed in the dw-kit repository under `/.dw/branding/` or marked as a Mark in this file
12
+
13
+ The Apache License 2.0 that governs the source code **does not** grant trademark rights (see Apache 2.0 §6). This file describes what you may and may not do with the Marks.
14
+
15
+ ## Permitted uses (no prior permission required)
16
+
17
+ You may, without asking:
18
+
19
+ 1. **Truthful descriptive reference.** Write articles, tutorials, blog posts, books, social posts, slide decks, or videos that mention `dw` or `dw-kit` to describe the project, compare it to alternatives, teach how to use it, or report on it. Examples:
20
+ - "We migrated our workflow from X to dw-kit"
21
+ - "dw-kit supports voice-driven agent orchestration"
22
+ - "Comparing dw vs Cursor for multi-agent coding"
23
+ 2. **Link to the official project.** Link to <https://github.com/dv-workflow/dv-workflow>, <https://www.npmjs.com/package/dw-kit>, or any official release announcement using the Marks as link text.
24
+ 3. **Use unmodified release artifacts.** Distribute the unmodified dw-kit npm package, source tarball, or container image with the Marks intact.
25
+ 4. **Internal use.** Use the Marks inside your organization (Slack channels, internal wikis, ticket systems, etc.) to refer to your deployment of dw-kit.
26
+ 5. **Conference talks, meetups, contributions.** Use the name when speaking about the project, contributing patches, filing issues, or running a user group.
27
+
28
+ ## Uses that require permission
29
+
30
+ Get written permission from the copyright holder before you:
31
+
32
+ 1. **Distribute a modified fork using a name that includes a Mark.** A fork is fine; calling it "dw-kit Pro", "Awesome DW", "dw-cloud", "DW Plus", or any variant that could confuse users into thinking it's an official release is not.
33
+ 2. **Offer a hosted service under a Mark.** Selling or operating a SaaS, paid hosted dashboard, or managed service named "dw Cloud", "DW Hosted", "dw.io", etc., or in a way that uses the Marks to imply official endorsement.
34
+ 3. **Sell goods under a Mark.** T-shirts, stickers, merchandise, or training products that prominently use the Marks for commercial purposes outside reasonable promotional give-aways.
35
+ 4. **Use the Marks in your company name, product name, domain name, or social media handle** in a way that implies the company/product/account is the official dw-kit project.
36
+
37
+ For permission, email <huygdv19@gmail.com> with a brief description of the proposed use, who you are, and the timeline.
38
+
39
+ ## Uses that are not permitted
40
+
41
+ Even with permission, the following remain prohibited:
42
+
43
+ 1. **Endorsement claims.** You may not state or imply that the dw-kit project, its maintainers, or its contributors endorse, certify, recommend, or are affiliated with your product, service, fork, training course, or business unless a written agreement exists.
44
+ 2. **Confusingly similar marks.** You may not adopt logos, color schemes, or wordmarks designed to be mistaken for the Marks. Calling a fork `dw-toolkit`, `dw-workflow-kit`, or `dw-prime` falls in this bucket.
45
+ 3. **Disparagement under cover.** You may not use the Marks in materials whose primary purpose is to disparage, mislead about, or harm the dw-kit project.
46
+
47
+ ## Fork naming guidance
48
+
49
+ Forks are explicitly encouraged by the Apache License. To name your fork in a way that does not require trademark permission:
50
+
51
+ - **Good:** `acme-agent-runtime` (your own name), `MyOrgAgentOS`, `agentctl`
52
+ - **Acceptable with attribution:** `acme-agent-runtime (based on dw-kit)`, with the relationship clearly disclosed
53
+ - **Requires permission:** Anything that puts `dw` or `dw-kit` in the package name, binary name, or marketing copy as if it were the canonical version
54
+
55
+ ## Why this policy exists
56
+
57
+ The Marks signal "this is the official dw-kit project, vetted and shipped by the original team." If a security issue, breaking change, or release coordination question comes up, users need a single trusted source. Trademark protection keeps that signal honest while leaving the Apache-licensed code maximally open for forking, modification, and redistribution.
58
+
59
+ If you are unsure whether your intended use is permitted, ask. The default answer for friendly, reasonable, honest requests is yes.
60
+
61
+ ## Updates to this policy
62
+
63
+ This policy may be updated. The current version always lives at <https://github.com/dv-workflow/dv-workflow/blob/main/TRADEMARK.md>. Material changes will be noted in `CHANGELOG.md`.
64
+
65
+ Last updated: 2026-05-26
package/bin/dw.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const [major] = process.versions.node.split('.').map(Number);
4
4
  if (major < 18) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dw-kit",
3
- "version": "1.8.0-rc.2",
3
+ "version": "1.9.0",
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/",
@@ -30,6 +31,8 @@
30
31
  ".claude/skills/dw-estimate/",
31
32
  ".claude/skills/dw-execute/",
32
33
  ".claude/skills/dw-flow/",
34
+ ".claude/skills/dw-goal/",
35
+ ".claude/skills/dw-goal-sync/",
33
36
  ".claude/skills/dw-handoff/",
34
37
  ".claude/skills/dw-kit-report/",
35
38
  ".claude/skills/dw-log-work/",
@@ -48,9 +51,11 @@
48
51
  ".claude/skills/dw-upgrade/",
49
52
  ".claude/templates/",
50
53
  ".claude/settings.json",
51
- "CLAUDE.md",
52
54
  "MIGRATION-v1.3.md",
53
- "MIGRATION-v1.5.md"
55
+ "MIGRATION-v1.5.md",
56
+ "NOTICE",
57
+ "TRADEMARK.md",
58
+ "SECURITY.md"
54
59
  ],
55
60
  "engines": {
56
61
  "node": ">=18"
@@ -59,7 +64,10 @@
59
64
  "test": "node src/smoke-test.mjs",
60
65
  "test:renderer": "cd packages/dw-kit-render && npm test",
61
66
  "link": "npm link",
62
- "test:e2e-local": "bash scripts/e2e-local-check.sh"
67
+ "test:e2e-local": "bash scripts/e2e-local-check.sh",
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"
63
71
  },
64
72
  "keywords": [
65
73
  "ai",
@@ -73,7 +81,7 @@
73
81
  "agent"
74
82
  ],
75
83
  "author": "huygdv <huygdv19@gmail.com>",
76
- "license": "MIT",
84
+ "license": "Apache-2.0",
77
85
  "repository": {
78
86
  "type": "git",
79
87
  "url": "git+https://github.com/dv-workflow/dv-workflow.git"
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
+ }
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { parseFrontmatter, stringifyFrontmatter } from '../lib/frontmatter.mjs';
5
5
  import { logEvent } from '../lib/telemetry.mjs';
6
+ import { bareSchemaVersion } from '../lib/task-store.mjs';
6
7
 
7
8
  const TASKS_DIR = '.dw/tasks';
8
9
 
@@ -72,7 +73,7 @@ function mergeFrontmatter(specFm, trackingFm) {
72
73
  depth: specFm.depth || 'standard',
73
74
  related_adr: String(specFm.related_adr || 'none').match(/^(ADR-\d{4}|none)$/) ? specFm.related_adr : 'none',
74
75
  target_ship: specFm.target_ship || 'TBD',
75
- schema_version: 'v3.0',
76
+ schema_version: 'task@v3.0',
76
77
  blockers: trackingFm.blockers || 'none',
77
78
  };
78
79
  }
@@ -308,7 +309,7 @@ function findV3Tasks(rootDir, targetSchemaVersion = 'v3.0') {
308
309
  const taskFile = join(e.path, 'task.md');
309
310
  if (!existsSync(taskFile)) return false;
310
311
  const fm = parseFrontmatter(readFileSync(taskFile, 'utf8'));
311
- return fm.schema_version === targetSchemaVersion;
312
+ return bareSchemaVersion(fm.schema_version) === bareSchemaVersion(targetSchemaVersion);
312
313
  } catch { return false; }
313
314
  });
314
315
  }
@@ -321,16 +322,16 @@ async function migrateOneToV31(taskDir, opts) {
321
322
  }
322
323
  const content = readFileSync(taskFile, 'utf8');
323
324
  const fm = parseFrontmatter(content);
324
- if (fm.schema_version === 'v3.1') {
325
+ if (bareSchemaVersion(fm.schema_version) === 'v3.1') {
325
326
  console.log(chalk.dim(` · ${taskDir} — already v3.1`));
326
327
  return { ok: true, noop: true };
327
328
  }
328
- if (fm.schema_version !== 'v3.0') {
329
+ if (bareSchemaVersion(fm.schema_version) !== 'v3.0') {
329
330
  console.log(chalk.yellow(` ⚠ ${taskDir} — schema_version=${fm.schema_version || 'missing'}, expected v3.0 — skipping`));
330
331
  return { ok: false, skipped: true };
331
332
  }
332
333
 
333
- const updated = { ...fm, schema_version: 'v3.1' };
334
+ const updated = { ...fm, schema_version: 'task@v3.1' };
334
335
  if (!('parent_goal_id' in updated)) updated.parent_goal_id = null;
335
336
  if (!('contributing_goal_ids' in updated)) updated.contributing_goal_ids = [];
336
337
  if (!('summary' in updated)) updated.summary = null;
@@ -457,6 +458,16 @@ export async function taskMigrateCommand(taskName, opts = {}) {
457
458
  dry_run: !!opts.dryRun,
458
459
  }, rootDir);
459
460
 
461
+ // Keep tasks-index@v1 current after a real migration (ADR-0017 doc contract).
462
+ if (!opts.dryRun && !opts.diff) {
463
+ try {
464
+ const { syncTaskIndexEntry } = await import('../lib/task-store.mjs');
465
+ for (const r of results) {
466
+ if (r.ok && !r.noop && !r.skipped) syncTaskIndexEntry(r.name, rootDir);
467
+ }
468
+ } catch { /* best-effort; `dw task index` rebuilds authoritatively */ }
469
+ }
470
+
460
471
  console.log();
461
472
  if (opts.dryRun) {
462
473
  console.log(chalk.cyan(` Dry-run complete. Re-run without --dry-run to apply.`));
@@ -76,6 +76,12 @@ export async function taskNewCommand(taskName, opts = {}) {
76
76
  const target = join(taskDir, 'task.md');
77
77
  writeFileSync(target, filled, 'utf8');
78
78
 
79
+ // Keep tasks-index@v1 current on creation (ADR-0017 doc contract).
80
+ try {
81
+ const { syncTaskIndexEntry } = await import('../lib/task-store.mjs');
82
+ syncTaskIndexEntry(slug, rootDir);
83
+ } catch { /* index is best-effort here; `dw task index` rebuilds authoritatively */ }
84
+
79
85
  logEvent({ event: 'task', action: 'new', name: slug, depth }, rootDir);
80
86
 
81
87
  console.log();
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { parseFrontmatter, stringifyFrontmatter } from '../lib/frontmatter.mjs';
5
5
  import { logEvent } from '../lib/telemetry.mjs';
6
+ import { bareSchemaVersion } from '../lib/task-store.mjs';
6
7
 
7
8
  const TASKS_DIR = '.dw/tasks';
8
9
  const MAX_SUMMARY_CHARS = 1000;
@@ -38,7 +39,7 @@ export async function taskSummaryCommand(taskName, opts = {}) {
38
39
  process.exit(1);
39
40
  }
40
41
  const updated = { ...fm, summary: opts.write || null, last_updated: todayIso() };
41
- if (updated.schema_version === 'v3.0') updated.schema_version = 'v3.1';
42
+ if (bareSchemaVersion(updated.schema_version) === 'v3.0') updated.schema_version = 'task@v3.1';
42
43
  const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
43
44
  writeFileSync(file, stringifyFrontmatter(updated) + body, 'utf8');
44
45
 
@@ -46,8 +47,8 @@ export async function taskSummaryCommand(taskName, opts = {}) {
46
47
 
47
48
  console.log();
48
49
  console.log(chalk.green(` ✓ Summary updated for ${chalk.bold(taskName)} (${(opts.write || '').length}/${MAX_SUMMARY_CHARS} chars)`));
49
- if (fm.schema_version === 'v3.0') {
50
- console.log(chalk.dim(` Auto-bumped schema_version v3.0 → v3.1`));
50
+ if (bareSchemaVersion(fm.schema_version) === 'v3.0') {
51
+ console.log(chalk.dim(` Auto-bumped schema_version v3.0 → task@v3.1`));
51
52
  }
52
53
  console.log();
53
54
  return;