avtc-pi-featyard 1.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/agents/fy-design-reviewer.md +77 -0
  4. package/agents/fy-feature-verifier.md +24 -0
  5. package/agents/fy-general-reviewer.md +81 -0
  6. package/agents/fy-guidelines-reviewer.md +46 -0
  7. package/agents/fy-implementer.md +20 -0
  8. package/agents/fy-performance-reviewer.md +46 -0
  9. package/agents/fy-plan-reviewer.md +74 -0
  10. package/agents/fy-plan-verifier.md +44 -0
  11. package/agents/fy-quality-reviewer.md +50 -0
  12. package/agents/fy-requirements-reviewer.md +46 -0
  13. package/agents/fy-researcher.md +6 -0
  14. package/agents/fy-security-reviewer.md +46 -0
  15. package/agents/fy-task-verifier.md +42 -0
  16. package/agents/fy-testing-reviewer.md +60 -0
  17. package/assets/images/kanban-board.png +0 -0
  18. package/assets/images/settings-dropdown.png +0 -0
  19. package/assets/images/settings.png +0 -0
  20. package/assets/images/widget.png +0 -0
  21. package/docs/CONFIGURATION.md +266 -0
  22. package/index.ts +8 -0
  23. package/package.json +85 -0
  24. package/skills/fy-design/SKILL.md +108 -0
  25. package/skills/fy-design-review/SKILL.md +45 -0
  26. package/skills/fy-finish/SKILL.md +28 -0
  27. package/skills/fy-implement/SKILL.md +36 -0
  28. package/skills/fy-plan/SKILL.md +77 -0
  29. package/skills/fy-plan-review/SKILL.md +45 -0
  30. package/skills/fy-research/SKILL.md +38 -0
  31. package/skills/fy-review/SKILL.md +88 -0
  32. package/skills/fy-verify/SKILL.md +23 -0
  33. package/src/commands/auto-agent-commands.ts +748 -0
  34. package/src/commands/kanban-commands.ts +278 -0
  35. package/src/commands/workflow-commands.ts +531 -0
  36. package/src/compaction/compact-handler.ts +371 -0
  37. package/src/compaction/compact-message.ts +52 -0
  38. package/src/compaction/compact-trigger.ts +121 -0
  39. package/src/compaction/safe-editor-write.ts +18 -0
  40. package/src/events/agent/agent-end.ts +19 -0
  41. package/src/events/agent/agent-settled.ts +20 -0
  42. package/src/events/agent/agent-start.ts +18 -0
  43. package/src/events/agent/turn-end.ts +20 -0
  44. package/src/events/agent/turn-start.ts +19 -0
  45. package/src/events/event-deps.ts +33 -0
  46. package/src/events/index.ts +91 -0
  47. package/src/events/input/input.ts +143 -0
  48. package/src/events/session/context.ts +68 -0
  49. package/src/events/session/model-select.ts +20 -0
  50. package/src/events/session/session-compact.ts +20 -0
  51. package/src/events/session/session-shutdown.ts +26 -0
  52. package/src/events/session/session-start.ts +45 -0
  53. package/src/events/session/session-tree.ts +17 -0
  54. package/src/events/tool/tool-call.ts +58 -0
  55. package/src/events/tool/tool-result.ts +54 -0
  56. package/src/git/git-queries.ts +154 -0
  57. package/src/git/resolve-base-branch.ts +173 -0
  58. package/src/git/worktrees/worktree-helpers.ts +142 -0
  59. package/src/git/worktrees/worktree-interception.ts +66 -0
  60. package/src/git/worktrees/worktree-lifecycle.ts +380 -0
  61. package/src/guardrails/completion-gating.ts +63 -0
  62. package/src/guardrails/file-classifier.ts +461 -0
  63. package/src/guardrails/force-add-guard.ts +312 -0
  64. package/src/guardrails/guardrail-tracker.ts +73 -0
  65. package/src/guardrails/guardrail-violations.ts +47 -0
  66. package/src/guardrails/guardrails-engine.ts +663 -0
  67. package/src/guardrails/session-guardrails.ts +75 -0
  68. package/src/guardrails/shell-decompose.ts +285 -0
  69. package/src/guardrails/tdd-enforcement.ts +122 -0
  70. package/src/guardrails/test-output.ts +272 -0
  71. package/src/index.ts +598 -0
  72. package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
  73. package/src/integrations/subagent-integration.ts +166 -0
  74. package/src/integrations/todo-integration.ts +58 -0
  75. package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
  76. package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
  77. package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
  78. package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
  79. package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
  80. package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
  81. package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
  82. package/src/kanban/data/kanban-database.ts +851 -0
  83. package/src/kanban/data/kanban-db-schema.ts +137 -0
  84. package/src/kanban/data/kanban-detect-project.ts +43 -0
  85. package/src/kanban/data/kanban-move-feature.ts +43 -0
  86. package/src/kanban/data/kanban-types.ts +64 -0
  87. package/src/kanban/ensure-feature.ts +226 -0
  88. package/src/kanban/kanban-board-ui/app.js +1125 -0
  89. package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
  90. package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
  91. package/src/kanban/kanban-board-ui/index.html +127 -0
  92. package/src/kanban/kanban-board-ui/style.css +820 -0
  93. package/src/kanban/kanban-bridge.ts +216 -0
  94. package/src/kanban/kanban-context.ts +43 -0
  95. package/src/kanban/kanban-events.ts +34 -0
  96. package/src/kanban/kanban-generate-title.ts +259 -0
  97. package/src/kanban/kanban-generate-topic.ts +170 -0
  98. package/src/kanban/kanban-operations.ts +170 -0
  99. package/src/kanban/kanban-server.ts +931 -0
  100. package/src/kanban/kanban-turn-handlers.ts +55 -0
  101. package/src/kanban/model-capture.ts +34 -0
  102. package/src/log.ts +47 -0
  103. package/src/phases/env-sync.ts +64 -0
  104. package/src/phases/execution-mode.ts +79 -0
  105. package/src/phases/implement-to-verify.ts +31 -0
  106. package/src/phases/phase-progression.ts +249 -0
  107. package/src/phases/phase-transitions.ts +226 -0
  108. package/src/phases/workflow-router.ts +98 -0
  109. package/src/prompts/finish-instructions.ts +143 -0
  110. package/src/prompts/skill-block-builder.ts +73 -0
  111. package/src/prompts/skill-expansion.ts +262 -0
  112. package/src/prompts/task-gate-skill.ts +83 -0
  113. package/src/prompts/template-engine.ts +433 -0
  114. package/src/prompts/text-blocks.ts +279 -0
  115. package/src/review/review-context.ts +56 -0
  116. package/src/review/review-counter.ts +66 -0
  117. package/src/review/review-empty-loop-tracking.ts +52 -0
  118. package/src/review/review-loops.ts +217 -0
  119. package/src/review/review-report.ts +101 -0
  120. package/src/settings/model-overrides.ts +269 -0
  121. package/src/settings/settings-schema.ts +458 -0
  122. package/src/settings/settings-types.ts +81 -0
  123. package/src/settings/settings-ui.ts +222 -0
  124. package/src/shared/errors.ts +24 -0
  125. package/src/shared/global.d.ts +22 -0
  126. package/src/shared/session-entries.ts +24 -0
  127. package/src/shared/types.ts +195 -0
  128. package/src/shared/workflow-refs.ts +253 -0
  129. package/src/shared/workflow-types.ts +180 -0
  130. package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
  131. package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
  132. package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
  133. package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
  134. package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
  135. package/src/state/archive-artifacts.ts +602 -0
  136. package/src/state/artifact-junction.ts +462 -0
  137. package/src/state/artifact-paths.ts +230 -0
  138. package/src/state/feature-activation.ts +239 -0
  139. package/src/state/feature-management.ts +325 -0
  140. package/src/state/feature-record-store.ts +39 -0
  141. package/src/state/feature-session.ts +263 -0
  142. package/src/state/feature-state.ts +439 -0
  143. package/src/state/post-turn-dispatch.ts +106 -0
  144. package/src/state/session-lifecycle.ts +346 -0
  145. package/src/state/state-persistence.ts +122 -0
  146. package/src/state/worth-notes.ts +89 -0
  147. package/src/tools/add-to-backlog.ts +117 -0
  148. package/src/tools/phase-ready.ts +742 -0
  149. package/src/tools/task-ready-advance.ts +235 -0
  150. package/src/tools/text-result.ts +15 -0
  151. package/src/ui/featyard-widget.ts +250 -0
  152. package/src/ui/manage-features-dialog.ts +246 -0
  153. package/src/ui/select-dialog.ts +27 -0
@@ -0,0 +1,137 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /*
5
+ * V2 scope — tag CRUD operations (schema tables exist, operations not yet implemented):
6
+ * - createTag(name, color?) → tag id
7
+ * - getTag(id) → Tag | null
8
+ * - listTags() → Tag[]
9
+ * - updateTag(id, { name?, color? }) → void
10
+ * - deleteTag(id) → void
11
+ * - addFeatureTag(featureId, tagId) → void
12
+ * - removeFeatureTag(featureId, tagId) → void
13
+ * - getFeatureTags(featureId) → Tag[]
14
+ * - getFeaturesByTag(tagId) → Feature[]
15
+ *
16
+ * The `tags` and `feature_tags` tables are created in V1 schema for forward
17
+ * compatibility, but no CRUD operations, REST endpoints, or UI elements
18
+ * exist yet. Implement when tag-based filtering and categorization is needed.
19
+ */
20
+
21
+ export const SCHEMA_V1 = `
22
+ -- Projects
23
+ CREATE TABLE IF NOT EXISTS projects (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ name TEXT NOT NULL,
26
+ repo_path TEXT NOT NULL UNIQUE,
27
+ base_branch TEXT,
28
+ created_at TEXT NOT NULL,
29
+ updated_at TEXT NOT NULL
30
+ );
31
+
32
+ -- Features
33
+ CREATE TABLE IF NOT EXISTS features (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ slug TEXT, -- nullable: null until design doc is written
36
+
37
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
38
+ lane TEXT NOT NULL DEFAULT 'backlog',
39
+ priority INTEGER NOT NULL DEFAULT 0,
40
+ title TEXT NOT NULL,
41
+ description TEXT,
42
+ design_doc TEXT,
43
+ created_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL
45
+ );
46
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_features_project_slug ON features(project_id, slug) WHERE slug IS NOT NULL;
47
+ CREATE INDEX IF NOT EXISTS idx_features_project_lane ON features(project_id, lane);
48
+ CREATE INDEX IF NOT EXISTS idx_features_priority ON features(project_id, lane, priority DESC);
49
+
50
+ -- Tags (many-to-many)
51
+ CREATE TABLE IF NOT EXISTS tags (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ name TEXT NOT NULL UNIQUE,
54
+ color TEXT
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS feature_tags (
58
+ feature_id INTEGER NOT NULL REFERENCES features(id) ON DELETE CASCADE,
59
+ tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
60
+ PRIMARY KEY (feature_id, tag_id)
61
+ );
62
+ CREATE INDEX IF NOT EXISTS idx_feature_tags_tag ON feature_tags(tag_id);
63
+
64
+ -- Dependencies (directed graph)
65
+ CREATE TABLE IF NOT EXISTS feature_dependencies (
66
+ feature_id INTEGER NOT NULL REFERENCES features(id) ON DELETE CASCADE,
67
+ depends_on_id INTEGER NOT NULL REFERENCES features(id) ON DELETE CASCADE,
68
+ kind TEXT NOT NULL CHECK(kind IN ('blocks', 'requires', 'related')),
69
+ PRIMARY KEY (feature_id, depends_on_id),
70
+ CHECK(feature_id != depends_on_id)
71
+ );
72
+ CREATE INDEX IF NOT EXISTS idx_feature_deps_from ON feature_dependencies(feature_id);
73
+ CREATE INDEX IF NOT EXISTS idx_feature_deps_to ON feature_dependencies(depends_on_id);
74
+
75
+ -- Feature history (audit trail)
76
+ CREATE TABLE IF NOT EXISTS feature_history (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ feature_id INTEGER NOT NULL REFERENCES features(id) ON DELETE CASCADE,
79
+ from_lane TEXT,
80
+ to_lane TEXT NOT NULL,
81
+ changed_by TEXT NOT NULL,
82
+ session_id TEXT,
83
+ note TEXT,
84
+ created_at TEXT NOT NULL
85
+ );
86
+ CREATE INDEX IF NOT EXISTS idx_feature_history_feature ON feature_history(feature_id);
87
+
88
+ -- Lock tracking
89
+ CREATE TABLE IF NOT EXISTS feature_locks (
90
+ feature_id INTEGER PRIMARY KEY REFERENCES features(id) ON DELETE CASCADE,
91
+ session_id TEXT NOT NULL,
92
+ locked_at TEXT NOT NULL,
93
+ last_heartbeat TEXT NOT NULL
94
+ );
95
+ CREATE INDEX IF NOT EXISTS idx_feature_locks_session ON feature_locks(session_id);
96
+
97
+ -- Migration tracking
98
+ CREATE TABLE IF NOT EXISTS schema_migrations (
99
+ version INTEGER PRIMARY KEY,
100
+ applied_at TEXT NOT NULL
101
+ );
102
+ `;
103
+
104
+ export const SCHEMA_V2 = `
105
+ -- Add overlay_status column for waiting-for-response indicator
106
+ ALTER TABLE features ADD COLUMN overlay_status TEXT DEFAULT NULL;
107
+ `;
108
+
109
+ export const SCHEMA_V3 = `
110
+ -- Auto-agent state persistence for crash recovery
111
+ CREATE TABLE IF NOT EXISTS auto_agent_state (
112
+ session_id TEXT PRIMARY KEY,
113
+ role TEXT NOT NULL CHECK(role IN ('worker', 'designer', 'agent')),
114
+ project_id INTEGER NOT NULL,
115
+ state TEXT NOT NULL CHECK(state IN ('idle', 'working', 'waiting', 'polling', 'stopped', 'error')),
116
+ current_feature_id INTEGER,
117
+ current_feature_lane TEXT,
118
+ stop_requested INTEGER NOT NULL DEFAULT 0,
119
+ created_at TEXT NOT NULL,
120
+ updated_at TEXT NOT NULL
121
+ );
122
+ `;
123
+
124
+ export const SCHEMA_V4 = `
125
+ -- Add metadata columns for cross-referencing and session tracking
126
+ ALTER TABLE features ADD COLUMN plan_doc TEXT DEFAULT NULL;
127
+ ALTER TABLE features ADD COLUMN state_file TEXT DEFAULT NULL;
128
+ ALTER TABLE features ADD COLUMN assigned_session TEXT DEFAULT NULL;
129
+ CREATE INDEX IF NOT EXISTS idx_features_assigned ON features(assigned_session);
130
+ `;
131
+
132
+ export const MIGRATIONS: { version: number; sql: string }[] = [
133
+ { version: 1, sql: SCHEMA_V1 },
134
+ { version: 2, sql: SCHEMA_V2 },
135
+ { version: 3, sql: SCHEMA_V3 },
136
+ { version: 4, sql: SCHEMA_V4 },
137
+ ];
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ import { log } from "../../log.js";
5
+ import type { KanbanDatabase } from "./kanban-database.js";
6
+
7
+ /**
8
+ * Resolve the main repo path from the given cwd.
9
+ * Handles worktrees by using git to find the main repo.
10
+ * Encapsulates the dynamic import of node:child_process + node:util + worktree.js.
11
+ */
12
+ export async function resolveRepoPath(cwd: string): Promise<string> {
13
+ const { exec } = await import("node:child_process");
14
+ const { promisify } = await import("node:util");
15
+ const execAsync = promisify(exec);
16
+ const { resolveMainRepoPath } = await import("../../git/worktrees/worktree-lifecycle.js");
17
+ const execFn: import("../../git/worktrees/worktree-lifecycle.js").ExecFn = async (cmd) => {
18
+ const { stdout } = await execAsync(cmd, { cwd });
19
+ return { exitCode: 0, stdout };
20
+ };
21
+ return resolveMainRepoPath(execFn);
22
+ }
23
+
24
+ /**
25
+ * Detect the kanban project ID for the given working directory.
26
+ * Resolves the main repo path (handles worktrees) and looks up the project.
27
+ */
28
+ export async function detectProject(database: KanbanDatabase, cwd: string): Promise<number | null> {
29
+ try {
30
+ const repoPath = await resolveRepoPath(cwd);
31
+ log.info(`[kanban] detectProject: cwd=${cwd}, resolved repoPath=${repoPath}`);
32
+ const project = database.findProjectByRepoPath(repoPath);
33
+ if (project) {
34
+ log.info(`[kanban] detectProject: found project ${project.id} "${project.name}" for repoPath=${repoPath}`);
35
+ } else {
36
+ log.info(`[kanban] detectProject: no project found for repoPath=${repoPath}`);
37
+ }
38
+ return project?.id ?? null;
39
+ } catch (err) {
40
+ log.warn(`[kanban] detectProject failed (cwd=${cwd}): ${err}`);
41
+ return null; // not a git repo or git not available
42
+ }
43
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Helper to move a kanban feature to a lane with null-safe database access.
6
+ *
7
+ * Every call site follows the same pattern:
8
+ * const { getDatabaseInstance } = await import("./kanban/index.js");
9
+ * const kanbanDb = getDatabaseInstance();
10
+ * if (kanbanDb) { kanbanDb.moveFeature({...}); }
11
+ *
12
+ * This helper collapses that boilerplate into a single call.
13
+ */
14
+
15
+ import { log } from "../../log.js";
16
+ import { getDatabaseInstance } from "../kanban-bridge.js";
17
+ import type { Lane } from "./kanban-types.js";
18
+
19
+ interface MoveFeatureOptions {
20
+ featureId: number;
21
+ toLane: Lane;
22
+ changedBy?: string;
23
+ note?: string;
24
+ fromLane: Lane | undefined;
25
+ }
26
+
27
+ /**
28
+ * Move a kanban feature to the specified lane.
29
+ * No-ops if the database is not initialized or featureId is null/undefined.
30
+ */
31
+ export async function moveFeatureToLane(opts: MoveFeatureOptions): Promise<void> {
32
+ const kanbanDb = getDatabaseInstance();
33
+ if (!kanbanDb) return;
34
+
35
+ kanbanDb.moveFeature({
36
+ featureId: opts.featureId,
37
+ toLane: opts.toLane,
38
+ changedBy: opts.changedBy ?? "system",
39
+ note: opts.note ?? "",
40
+ fromLane: opts.fromLane,
41
+ });
42
+ log.info(`kanban: moved feature ${opts.featureId} to ${opts.toLane} lane`);
43
+ }
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ export type Lane = "backlog" | "design" | "design-approval" | "ready" | "in-progress" | "uat" | "done";
5
+
6
+ /** Canonical lane order — single source of truth for all lane ordering. */
7
+ export const LANE_ORDER: readonly Lane[] = [
8
+ "backlog",
9
+ "design",
10
+ "design-approval",
11
+ "ready",
12
+ "in-progress",
13
+ "uat",
14
+ "done",
15
+ ] as const;
16
+
17
+ /** Set of valid lane values for validation. */
18
+ export const VALID_LANES: ReadonlySet<Lane> = new Set(LANE_ORDER);
19
+
20
+ /** Type guard: narrows `string` to `Lane` using VALID_LANES. */
21
+ export function isLane(value: string | undefined): value is Lane {
22
+ return value !== undefined && VALID_LANES.has(value as Lane);
23
+ }
24
+
25
+ export interface Project {
26
+ id: number;
27
+ name: string;
28
+ repo_path: string;
29
+ base_branch: string | null;
30
+ created_at: string;
31
+ updated_at: string;
32
+ }
33
+
34
+ export interface Feature {
35
+ id: number;
36
+ slug: string | null;
37
+ project_id: number;
38
+ lane: Lane;
39
+ priority: number;
40
+ title: string;
41
+ description: string | null;
42
+ design_doc: string | null;
43
+ plan_doc: string | null;
44
+ state_file: string | null;
45
+ assigned_session: string | null;
46
+ overlay_status: string | null; // null | 'waiting-for-response'
47
+ created_at: string;
48
+ updated_at: string;
49
+ // Joined from feature_locks when present
50
+ locked_at?: string | null;
51
+ last_heartbeat?: string | null;
52
+ locked_by_session?: string | null; // fl.session_id from feature_locks
53
+ }
54
+
55
+ export interface FeatureHistoryEntry {
56
+ id: number;
57
+ feature_id: number;
58
+ from_lane: string | null;
59
+ to_lane: string;
60
+ changed_by: string;
61
+ session_id: string | null;
62
+ note: string | null;
63
+ created_at: string;
64
+ }
@@ -0,0 +1,226 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Kanban ↔ Feature State linking hooks.
6
+ *
7
+ * Provides helpers that bridge the kanban database and per-feature state files,
8
+ * used when design docs are written or features are picked by auto-agent.
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { log } from "../log.js";
14
+ import type { FeatureState } from "../state/feature-state.js";
15
+ import { DEFAULT_DIR, getDesignDoc, saveFeatureState, stateDir, stateFilePath } from "../state/feature-state.js";
16
+ import type { KanbanDatabase } from "./data/kanban-database.js";
17
+ import type { Lane } from "./data/kanban-types.js";
18
+ import { generateFeatureMeta } from "./kanban-generate-title.js";
19
+
20
+ /**
21
+ * Extract a human-readable title from a slug by removing the date prefix and capitalizing words.
22
+ * Used as fallback when LLM title generation is not available.
23
+ *
24
+ * "2026-05-22-auto-agent-notifications-and-widget-improvement" → "Auto Agent Notifications And Widget Improvement"
25
+ */
26
+ function titleFromSlug(slug: string): string {
27
+ // Remove date prefix (YYYY-MM-DD-)
28
+ const withoutDate = slug.replace(/^\d{4}-\d{2}-\d{2}-/, "");
29
+ // Capitalize words, replace hyphens with spaces
30
+ return withoutDate
31
+ .split("-")
32
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
33
+ .join(" ");
34
+ }
35
+
36
+ /**
37
+ * Extract the first heading and paragraph from a markdown design doc.
38
+ * Returns { title, description } or null if the file can't be read.
39
+ */
40
+ function extractMetaFromDesignDoc(designDocPath: string | null): { title: string; description: string } | null {
41
+ if (!designDocPath || !fs.existsSync(designDocPath)) return null;
42
+ try {
43
+ const content = fs.readFileSync(designDocPath, "utf-8");
44
+ // Extract first heading (# Title)
45
+ const titleMatch = content.match(/^#\s+(.+)$/m);
46
+ const title = titleMatch?.[1]?.trim();
47
+ // Extract first non-heading, non-empty paragraph
48
+ const lines = content.split("\n");
49
+ let description = "";
50
+ let pastFrontmatter = false;
51
+ for (const line of lines) {
52
+ const trimmed = line.trim();
53
+ // Skip frontmatter
54
+ if (!pastFrontmatter && trimmed === "---") {
55
+ pastFrontmatter = true;
56
+ continue;
57
+ }
58
+ if (pastFrontmatter && trimmed === "---") {
59
+ pastFrontmatter = false;
60
+ continue;
61
+ }
62
+ if (pastFrontmatter) continue;
63
+ // Skip headings and empty lines
64
+ if (trimmed.startsWith("#") || trimmed === "") continue;
65
+ // Skip metadata lines (Date:, Status:, Branch:, **Date**, etc.)
66
+ if (/^\*?\*?(Date|Status|Branch|Author)/i.test(trimmed)) continue;
67
+ if (/^---/.test(trimmed)) continue;
68
+ description = trimmed;
69
+ break;
70
+ }
71
+ if (title || description) {
72
+ return { title: title ?? "", description: description ?? "" };
73
+ }
74
+ } catch {
75
+ // Can't read file — that's fine
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Ensure a kanban feature exists for the given feature state.
82
+ *
83
+ * If the state already has kanbanFeatureId, no-ops.
84
+ * If a kanban feature with matching slug exists, links it.
85
+ * Otherwise, finds or auto-creates a project for cwd and adds the feature
86
+ * to the backlog lane with a proper title and description.
87
+ *
88
+ * When auto-creating a feature (user started working in TUI without adding to kanban):
89
+ * - Uses LLM to generate title + description from design doc content
90
+ * - Falls back to extracting title/description from markdown headings/paragraphs
91
+ * - Final fallback: derive title from slug
92
+ *
93
+ * Mutates `state` in place (sets `state.featureId`) and persists it to disk.
94
+ * The caller passes its sole-owner record and adopts it directly — consistent
95
+ * with the handler's single-source-of-truth model.
96
+ */
97
+ export async function ensureKanbanFeature(
98
+ db: KanbanDatabase,
99
+ state: FeatureState,
100
+ resolvedCwd: string | null,
101
+ sessionId: string | null,
102
+ targetLane: Lane,
103
+ ): Promise<void> {
104
+ // Already linked — no-op
105
+ if (state.featureId !== null) {
106
+ return;
107
+ }
108
+
109
+ // Find or create project for current cwd (or resolved main repo path if in worktree)
110
+ const cwd = resolvedCwd ?? process.cwd();
111
+ let project = db.findProjectByRepoPath(cwd);
112
+ if (!project) {
113
+ const projectId = db.createProject({
114
+ name: path.basename(cwd),
115
+ repoPath: cwd,
116
+ });
117
+ project = {
118
+ id: projectId,
119
+ name: path.basename(cwd),
120
+ repo_path: cwd,
121
+ base_branch: null,
122
+ created_at: new Date().toISOString(),
123
+ updated_at: new Date().toISOString(),
124
+ };
125
+ }
126
+
127
+ // Try to find existing kanban feature by slug scoped to this project
128
+ const existing = db.findFeatureBySlug(state.featureSlug, project.id);
129
+ if (existing) {
130
+ state.featureId = existing.id;
131
+ // Update state_file if not already set
132
+ if (!existing.state_file) {
133
+ const sf = stateFilePath(state.featureSlug, stateDir());
134
+ db.updateFeature({ featureId: existing.id, stateFile: sf });
135
+ }
136
+ // Lock FIRST if sessionId provided — prevents race where another agent picks up
137
+ // the feature between the move-to-target-lane and the lock attempt
138
+ if (sessionId) {
139
+ const locked = db.lockFeature(existing.id, sessionId);
140
+ if (!locked) {
141
+ log.warn(`[kanban-hooks] failed to lock feature ${existing.id} — already locked`);
142
+ } else {
143
+ db.updateFeature({ featureId: existing.id, assignedSession: sessionId });
144
+ }
145
+ }
146
+ // Move to the target lane if the feature is still in backlog (design doc ->
147
+ // "design", task-plan -> "in-progress"). Called from a doc-write handler, so the
148
+ // triggering doc always exists.
149
+ if (existing.lane === "backlog") {
150
+ db.moveFeature({
151
+ featureId: existing.id,
152
+ toLane: targetLane,
153
+ changedBy: "workflow-monitor",
154
+ note: `Doc written — moving from backlog to ${targetLane}`,
155
+ });
156
+ }
157
+ saveFeatureState(state, DEFAULT_DIR);
158
+ return;
159
+ }
160
+
161
+ // Auto-create feature with proper title and description
162
+ const designDocPath = getDesignDoc(state);
163
+ let title = titleFromSlug(state.featureSlug);
164
+ let description: string | null = null;
165
+
166
+ // Try to extract meta from design doc content
167
+ const docMeta = extractMetaFromDesignDoc(designDocPath);
168
+ if (docMeta?.title) {
169
+ title = docMeta.title;
170
+ }
171
+ if (docMeta?.description) {
172
+ description = docMeta.description;
173
+ }
174
+
175
+ // Try LLM-powered title + description generation
176
+ try {
177
+ const llmMeta = await generateFeatureMeta(designDocPath, title);
178
+ if (llmMeta.title) title = llmMeta.title;
179
+ if (llmMeta.description) description = llmMeta.description;
180
+ } catch (err) {
181
+ log.info(
182
+ `[kanban-hooks] LLM title/description generation failed, using fallback: ${
183
+ err instanceof Error ? err.message : err
184
+ }`,
185
+ );
186
+ }
187
+
188
+ // Auto-create feature in backlog first, then lock + move to the target lane if
189
+ // the triggering artifact exists. This avoids a race where the feature sits in
190
+ // the target lane unlocked between creation and lock acquisition — no agent picks
191
+ // from backlog, so it's safe there.
192
+ const featureId = db.createFeature({
193
+ projectId: project.id,
194
+ slug: state.featureSlug,
195
+ title,
196
+ description,
197
+ lane: "backlog",
198
+ designDoc: designDocPath ?? null,
199
+ });
200
+
201
+ // Set state_file path
202
+ const sf = stateFilePath(state.featureSlug, stateDir());
203
+ db.updateFeature({ featureId, stateFile: sf });
204
+
205
+ // Lock first (safe — still in backlog, no agent picks from there)
206
+ if (sessionId) {
207
+ const locked = db.lockFeature(featureId, sessionId);
208
+ if (!locked) {
209
+ log.warn(`[kanban-hooks] failed to lock feature ${featureId} — already locked`);
210
+ } else {
211
+ db.updateFeature({ featureId, assignedSession: sessionId });
212
+ }
213
+ }
214
+
215
+ // Now move to the target lane — feature is locked, safe to move
216
+ // (design doc -> "design", task-plan -> "in-progress").
217
+ db.moveFeature({
218
+ featureId,
219
+ toLane: targetLane,
220
+ changedBy: "workflow-monitor",
221
+ note: `Doc written — moving from backlog to ${targetLane}`,
222
+ });
223
+
224
+ state.featureId = featureId;
225
+ saveFeatureState(state, DEFAULT_DIR);
226
+ }