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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/agents/fy-design-reviewer.md +77 -0
- package/agents/fy-feature-verifier.md +24 -0
- package/agents/fy-general-reviewer.md +81 -0
- package/agents/fy-guidelines-reviewer.md +46 -0
- package/agents/fy-implementer.md +20 -0
- package/agents/fy-performance-reviewer.md +46 -0
- package/agents/fy-plan-reviewer.md +74 -0
- package/agents/fy-plan-verifier.md +44 -0
- package/agents/fy-quality-reviewer.md +50 -0
- package/agents/fy-requirements-reviewer.md +46 -0
- package/agents/fy-researcher.md +6 -0
- package/agents/fy-security-reviewer.md +46 -0
- package/agents/fy-task-verifier.md +42 -0
- package/agents/fy-testing-reviewer.md +60 -0
- package/assets/images/kanban-board.png +0 -0
- package/assets/images/settings-dropdown.png +0 -0
- package/assets/images/settings.png +0 -0
- package/assets/images/widget.png +0 -0
- package/docs/CONFIGURATION.md +266 -0
- package/index.ts +8 -0
- package/package.json +85 -0
- package/skills/fy-design/SKILL.md +108 -0
- package/skills/fy-design-review/SKILL.md +45 -0
- package/skills/fy-finish/SKILL.md +28 -0
- package/skills/fy-implement/SKILL.md +36 -0
- package/skills/fy-plan/SKILL.md +77 -0
- package/skills/fy-plan-review/SKILL.md +45 -0
- package/skills/fy-research/SKILL.md +38 -0
- package/skills/fy-review/SKILL.md +88 -0
- package/skills/fy-verify/SKILL.md +23 -0
- package/src/commands/auto-agent-commands.ts +748 -0
- package/src/commands/kanban-commands.ts +278 -0
- package/src/commands/workflow-commands.ts +531 -0
- package/src/compaction/compact-handler.ts +371 -0
- package/src/compaction/compact-message.ts +52 -0
- package/src/compaction/compact-trigger.ts +121 -0
- package/src/compaction/safe-editor-write.ts +18 -0
- package/src/events/agent/agent-end.ts +19 -0
- package/src/events/agent/agent-settled.ts +20 -0
- package/src/events/agent/agent-start.ts +18 -0
- package/src/events/agent/turn-end.ts +20 -0
- package/src/events/agent/turn-start.ts +19 -0
- package/src/events/event-deps.ts +33 -0
- package/src/events/index.ts +91 -0
- package/src/events/input/input.ts +143 -0
- package/src/events/session/context.ts +68 -0
- package/src/events/session/model-select.ts +20 -0
- package/src/events/session/session-compact.ts +20 -0
- package/src/events/session/session-shutdown.ts +26 -0
- package/src/events/session/session-start.ts +45 -0
- package/src/events/session/session-tree.ts +17 -0
- package/src/events/tool/tool-call.ts +58 -0
- package/src/events/tool/tool-result.ts +54 -0
- package/src/git/git-queries.ts +154 -0
- package/src/git/resolve-base-branch.ts +173 -0
- package/src/git/worktrees/worktree-helpers.ts +142 -0
- package/src/git/worktrees/worktree-interception.ts +66 -0
- package/src/git/worktrees/worktree-lifecycle.ts +380 -0
- package/src/guardrails/completion-gating.ts +63 -0
- package/src/guardrails/file-classifier.ts +461 -0
- package/src/guardrails/force-add-guard.ts +312 -0
- package/src/guardrails/guardrail-tracker.ts +73 -0
- package/src/guardrails/guardrail-violations.ts +47 -0
- package/src/guardrails/guardrails-engine.ts +663 -0
- package/src/guardrails/session-guardrails.ts +75 -0
- package/src/guardrails/shell-decompose.ts +285 -0
- package/src/guardrails/tdd-enforcement.ts +122 -0
- package/src/guardrails/test-output.ts +272 -0
- package/src/index.ts +598 -0
- package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
- package/src/integrations/subagent-integration.ts +166 -0
- package/src/integrations/todo-integration.ts +58 -0
- package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
- package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
- package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
- package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
- package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
- package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
- package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
- package/src/kanban/data/kanban-database.ts +851 -0
- package/src/kanban/data/kanban-db-schema.ts +137 -0
- package/src/kanban/data/kanban-detect-project.ts +43 -0
- package/src/kanban/data/kanban-move-feature.ts +43 -0
- package/src/kanban/data/kanban-types.ts +64 -0
- package/src/kanban/ensure-feature.ts +226 -0
- package/src/kanban/kanban-board-ui/app.js +1125 -0
- package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
- package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
- package/src/kanban/kanban-board-ui/index.html +127 -0
- package/src/kanban/kanban-board-ui/style.css +820 -0
- package/src/kanban/kanban-bridge.ts +216 -0
- package/src/kanban/kanban-context.ts +43 -0
- package/src/kanban/kanban-events.ts +34 -0
- package/src/kanban/kanban-generate-title.ts +259 -0
- package/src/kanban/kanban-generate-topic.ts +170 -0
- package/src/kanban/kanban-operations.ts +170 -0
- package/src/kanban/kanban-server.ts +931 -0
- package/src/kanban/kanban-turn-handlers.ts +55 -0
- package/src/kanban/model-capture.ts +34 -0
- package/src/log.ts +47 -0
- package/src/phases/env-sync.ts +64 -0
- package/src/phases/execution-mode.ts +79 -0
- package/src/phases/implement-to-verify.ts +31 -0
- package/src/phases/phase-progression.ts +249 -0
- package/src/phases/phase-transitions.ts +226 -0
- package/src/phases/workflow-router.ts +98 -0
- package/src/prompts/finish-instructions.ts +143 -0
- package/src/prompts/skill-block-builder.ts +73 -0
- package/src/prompts/skill-expansion.ts +262 -0
- package/src/prompts/task-gate-skill.ts +83 -0
- package/src/prompts/template-engine.ts +433 -0
- package/src/prompts/text-blocks.ts +279 -0
- package/src/review/review-context.ts +56 -0
- package/src/review/review-counter.ts +66 -0
- package/src/review/review-empty-loop-tracking.ts +52 -0
- package/src/review/review-loops.ts +217 -0
- package/src/review/review-report.ts +101 -0
- package/src/settings/model-overrides.ts +269 -0
- package/src/settings/settings-schema.ts +458 -0
- package/src/settings/settings-types.ts +81 -0
- package/src/settings/settings-ui.ts +222 -0
- package/src/shared/errors.ts +24 -0
- package/src/shared/global.d.ts +22 -0
- package/src/shared/session-entries.ts +24 -0
- package/src/shared/types.ts +195 -0
- package/src/shared/workflow-refs.ts +253 -0
- package/src/shared/workflow-types.ts +180 -0
- package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
- package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
- package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
- package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
- package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
- package/src/state/archive-artifacts.ts +602 -0
- package/src/state/artifact-junction.ts +462 -0
- package/src/state/artifact-paths.ts +230 -0
- package/src/state/feature-activation.ts +239 -0
- package/src/state/feature-management.ts +325 -0
- package/src/state/feature-record-store.ts +39 -0
- package/src/state/feature-session.ts +263 -0
- package/src/state/feature-state.ts +439 -0
- package/src/state/post-turn-dispatch.ts +106 -0
- package/src/state/session-lifecycle.ts +346 -0
- package/src/state/state-persistence.ts +122 -0
- package/src/state/worth-notes.ts +89 -0
- package/src/tools/add-to-backlog.ts +117 -0
- package/src/tools/phase-ready.ts +742 -0
- package/src/tools/task-ready-advance.ts +235 -0
- package/src/tools/text-result.ts +15 -0
- package/src/ui/featyard-widget.ts +250 -0
- package/src/ui/manage-features-dialog.ts +246 -0
- 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
|
+
}
|