@njeybe/nativ 2.5.1 → 2.7.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/CHANGELOG.md +67 -0
- package/README.md +100 -5
- package/dist/commands/design-check.d.ts +4 -0
- package/dist/commands/design-check.js +76 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +55 -11
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +25 -0
- package/dist/commands/session.d.ts +22 -0
- package/dist/commands/session.js +106 -0
- package/dist/commands/task/task-add.js +1 -1
- package/dist/commands/task/task-common.js +6 -6
- package/dist/commands/task/task-escalate.js +8 -10
- package/dist/commands/task/task-lifecycle.d.ts +7 -1
- package/dist/commands/task/task-lifecycle.js +30 -5
- package/dist/commands/task/task-query.d.ts +2 -0
- package/dist/commands/task/task-query.js +27 -12
- package/dist/commands/task/task-reclaim.d.ts +20 -0
- package/dist/commands/task/task-reclaim.js +87 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +1 -0
- package/dist/commands/triage.js +3 -3
- package/dist/commands/update.js +46 -0
- package/dist/commands/validate.js +1 -0
- package/dist/commands/verify.js +21 -5
- package/dist/core/design-rules.d.ts +23 -0
- package/dist/core/design-rules.js +115 -0
- package/dist/core/enforcement.js +18 -17
- package/dist/core/harness-integrity.js +26 -7
- package/dist/core/lock-manager.d.ts +6 -0
- package/dist/core/lock-manager.js +50 -41
- package/dist/core/notify.d.ts +38 -0
- package/dist/core/notify.js +133 -0
- package/dist/core/profiles.js +7 -16
- package/dist/core/session-memory.d.ts +42 -0
- package/dist/core/session-memory.js +121 -0
- package/dist/core/setup/pm-playbook.d.ts +5 -0
- package/dist/core/setup/pm-playbook.js +26 -0
- package/dist/core/setup/session-context.js +35 -2
- package/dist/core/setup/setup-planner.js +2 -2
- package/dist/core/setup/types.d.ts +2 -0
- package/dist/core/setup/types.js +5 -1
- package/dist/core/task-claims.d.ts +15 -0
- package/dist/core/task-claims.js +32 -0
- package/dist/core/tier1-liaison.d.ts +1 -0
- package/dist/core/tier1-liaison.js +25 -27
- package/dist/core/verifier/output-digest.d.ts +19 -0
- package/dist/core/verifier/output-digest.js +48 -0
- package/dist/db/data-engine.js +33 -32
- package/dist/db/introspector.js +61 -60
- package/dist/db/sqlite.d.ts +3 -0
- package/dist/db/sqlite.js +10 -0
- package/dist/governor/circuit-breaker.js +12 -0
- package/dist/governor/patch-applier.d.ts +1 -0
- package/dist/governor/patch-applier.js +3 -7
- package/dist/index.js +40 -6
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +53 -14
- package/dist/providers/claude-api.js +3 -0
- package/dist/providers/gemini.js +4 -1
- package/dist/providers/registry.d.ts +6 -2
- package/dist/providers/registry.js +28 -16
- package/dist/providers/types.d.ts +4 -1
- package/dist/runner/agent-supervisor.js +2 -2
- package/dist/runner/cli-config.js +1 -1
- package/dist/scanner/types.d.ts +5 -1
- package/dist/scanner/types.js +7 -0
- package/dist/server/routes/task-routes.d.ts +2 -0
- package/dist/server/routes/triage-routes.js +3 -18
- package/dist/server/studio-ui/pages/benchmarks/styles.js +8 -5
- package/dist/server/studio-ui/pages/database/browser.js +1 -1
- package/dist/server/studio-ui/pages/database/connection.js +1 -1
- package/dist/server/studio-ui/pages/database/data-shell.js +5 -5
- package/dist/server/studio-ui/pages/database/drift.js +1 -1
- package/dist/server/studio-ui/pages/database/explorer.js +2 -2
- package/dist/server/studio-ui/pages/database/styles.js +5 -5
- package/dist/server/studio-ui/pages/flow/styles.js +3 -3
- package/dist/server/studio-ui/pages/home/script.js +110 -91
- package/dist/server/studio-ui/pages/home/styles.js +33 -45
- package/dist/server/studio-ui/pages/tasks/script.js +150 -3
- package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
- package/dist/server/studio-ui/pages/team/script.js +49 -52
- package/dist/server/studio-ui/pages/team/styles.js +21 -20
- package/dist/server/studio-ui/pages/worktrees/script.js +47 -35
- package/dist/server/studio-ui/pages/worktrees/styles.js +12 -10
- package/dist/server/studio-ui/shell/brand.d.ts +4 -0
- package/dist/server/studio-ui/shell/brand.js +16 -0
- package/dist/server/studio-ui/shell/console.js +61 -2
- package/dist/server/studio-ui/shell/core.js +8 -8
- package/dist/server/studio-ui/shell/detail-view.js +1 -1
- package/dist/server/studio-ui/shell/dispatch.js +18 -0
- package/dist/server/studio-ui/shell/events-tools.js +5 -1
- package/dist/server/studio-ui/shell/format.js +5 -6
- package/dist/server/studio-ui/shell/icons.js +23 -15
- package/dist/server/studio-ui/shell/markup-head.js +11 -6
- package/dist/server/studio-ui/shell/markup-tools.js +19 -10
- package/dist/server/studio-ui/shell/paint.js +3 -0
- package/dist/server/studio-ui/shell/pipeline-state.js +1 -1
- package/dist/server/studio-ui/shell/proposals.js +6 -2
- package/dist/server/studio-ui/shell/pulse.d.ts +1 -0
- package/dist/server/studio-ui/shell/pulse.js +67 -0
- package/dist/server/studio-ui/shell/router.js +25 -0
- package/dist/server/studio-ui/shell/sidebar-view.js +6 -3
- package/dist/server/studio-ui/shell/toasts-format.js +4 -4
- package/dist/server/studio-ui/styles/console.js +33 -13
- package/dist/server/studio-ui/styles/dialogs.js +4 -4
- package/dist/server/studio-ui/styles/responsive.js +8 -0
- package/dist/server/studio-ui/styles/shell.js +24 -5
- package/dist/server/studio-ui/styles/strategist.js +10 -10
- package/dist/server/studio-ui/styles/tokens-base.js +23 -23
- package/dist/server/studio-ui.js +2 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +11 -12
- package/plugin/agents/explorer.md +1 -1
- package/plugin/agents/verifier.md +2 -2
- package/plugin/agents/worker.md +5 -7
- package/plugin/skills/nativ/SKILL.md +36 -49
- package/templates/AGENTS.md +36 -49
- package/templates/CLAUDE.md +1 -41
- package/templates/GEMINI.md +3 -2
- package/templates/claude-agents/architect.md +11 -12
- package/templates/claude-agents/explorer.md +1 -1
- package/templates/claude-agents/verifier.md +2 -2
- package/templates/claude-agents/worker.md +5 -7
- package/templates/design/taste/LICENSE +21 -0
- package/templates/design/taste/SOURCE.md +28 -0
- package/templates/design/taste/v1/DIGEST.md +57 -0
- package/templates/design/taste/v1/SKILL.md +226 -0
- package/templates/design/taste/v2/DIGEST.md +62 -0
- package/templates/design/taste/v2/SKILL.md +1206 -0
- package/templates/dot-ai/api_contracts.json +66 -66
- package/templates/dot-ai/context.md +38 -38
- package/templates/dot-ai/db_schema.json +39 -39
- package/templates/dot-ai/subagents/architect.md +6 -0
- package/templates/dot-ai/subagents/flutter-developer.md +2 -11
- package/templates/dot-ai/subagents/frontend.md +47 -44
- package/templates/dot-ai/ui_specs.md +15 -36
|
@@ -24,22 +24,22 @@ export function getRoleGuide(assignedSubagent) {
|
|
|
24
24
|
export function getRecommendedContractSlice(assignedSubagent) {
|
|
25
25
|
const sub = assignedSubagent.toLowerCase();
|
|
26
26
|
if (sub.includes('frontend') || sub.includes('flutter')) {
|
|
27
|
-
return '.ai/ui_specs.md
|
|
27
|
+
return '.ai/ui_specs.md + .ai/api_contracts.json';
|
|
28
28
|
}
|
|
29
29
|
if (sub.includes('backend')) {
|
|
30
|
-
return '.ai/api_contracts.json
|
|
30
|
+
return '.ai/api_contracts.json + .ai/db_schema.json';
|
|
31
31
|
}
|
|
32
32
|
if (sub.includes('database') || sub.includes('db-migration')) {
|
|
33
|
-
return '.ai/db_schema.json
|
|
33
|
+
return '.ai/db_schema.json';
|
|
34
34
|
}
|
|
35
35
|
if (sub.includes('devops')) {
|
|
36
|
-
return '.ai/context.md
|
|
36
|
+
return '.ai/context.md';
|
|
37
37
|
}
|
|
38
38
|
if (sub.includes('security')) {
|
|
39
|
-
return '
|
|
39
|
+
return 'targetFiles + dependency manifests + .ai/context.md';
|
|
40
40
|
}
|
|
41
41
|
if (sub.includes('qa')) {
|
|
42
|
-
return '
|
|
42
|
+
return 'targetFiles + .ai/api_contracts.json';
|
|
43
43
|
}
|
|
44
44
|
return '.ai/context.md';
|
|
45
45
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
+
import { ESCALATION_TYPES, } from '../../scanner/types.js';
|
|
3
4
|
import { ContractGovernor, appendEscalation, } from '../../governor/index.js';
|
|
4
5
|
import { withPlanLock } from '../../core/lock-manager.js';
|
|
5
6
|
import { getPlanPath } from './task-common.js';
|
|
6
7
|
export async function runTaskEscalate(taskId, targetDirArg, options = {}) {
|
|
8
|
+
const requestedType = options.type?.trim();
|
|
9
|
+
if (requestedType && !ESCALATION_TYPES.includes(requestedType)) {
|
|
10
|
+
console.error(pc.red(`\n✖ Unknown escalation type "${requestedType}". Use one of: ${ESCALATION_TYPES.join(', ')}\n`));
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const escType = (requestedType || 'architectural_ambiguity');
|
|
7
15
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
8
16
|
return withPlanLock(planPath, (plan, ctx) => {
|
|
9
17
|
let foundTask = null;
|
|
@@ -20,16 +28,6 @@ export async function runTaskEscalate(taskId, targetDirArg, options = {}) {
|
|
|
20
28
|
process.exitCode = 1;
|
|
21
29
|
return;
|
|
22
30
|
}
|
|
23
|
-
const validTypes = [
|
|
24
|
-
'contract_drift',
|
|
25
|
-
'schema_flaw',
|
|
26
|
-
'missing_credential',
|
|
27
|
-
'dependency_conflict',
|
|
28
|
-
'architectural_ambiguity',
|
|
29
|
-
];
|
|
30
|
-
const escType = (options.type && validTypes.includes(options.type))
|
|
31
|
-
? options.type
|
|
32
|
-
: 'architectural_ambiguity';
|
|
33
31
|
const affectedContracts = options.affected
|
|
34
32
|
? options.affected.split(',').map((s) => s.trim())
|
|
35
33
|
: ['.ai/db_schema.json', '.ai/api_contracts.json'];
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { TaskUnlockOptions, TaskCompleteOptions } from './task-common.js';
|
|
2
|
-
export
|
|
2
|
+
export interface TaskStartOptions {
|
|
3
|
+
/** Who claims the task; defaults to NATIV_AGENT_ID. */
|
|
4
|
+
agent?: string;
|
|
5
|
+
/** Human override: start a blocked task or one whose dependencies are unfinished. */
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function runTaskStart(taskId: string, targetDirArg?: string, options?: TaskStartOptions): Promise<void | null>;
|
|
3
9
|
export declare function runTaskUnlock(taskId: string, targetDirArg?: string, options?: TaskUnlockOptions): Promise<void>;
|
|
4
10
|
export declare function runTaskComplete(taskId: string, targetDirArg?: string, options?: TaskCompleteOptions): Promise<void>;
|
|
5
11
|
export declare function runTaskBlock(taskId: string, reason: string, targetDirArg?: string): Promise<void | null>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { runTaskVerification } from '../../core/verifier.js';
|
|
3
|
+
import { digestFailure } from '../../core/verifier/output-digest.js';
|
|
3
4
|
import { CircuitBreaker, TestIntegrityGuard, resolveGitHead, closeEscalationsForTask, } from '../../governor/index.js';
|
|
4
5
|
import { loadPlan, withPlanLock } from '../../core/lock-manager.js';
|
|
5
6
|
import { recordTaskStart, recordTaskComplete } from '../../core/telemetry.js';
|
|
6
7
|
import { addTaskUnlock, removeTaskUnlock } from '../../core/enforcement.js';
|
|
7
8
|
import { refuseHeadless } from '../../core/human-gate.js';
|
|
9
|
+
import { resolveAgentId, startRefusal } from '../../core/task-claims.js';
|
|
8
10
|
import { getPlanPath, } from './task-common.js';
|
|
9
|
-
export async function runTaskStart(taskId, targetDirArg) {
|
|
11
|
+
export async function runTaskStart(taskId, targetDirArg, options = {}) {
|
|
12
|
+
if (options.force && refuseHeadless('nativ task start --force'))
|
|
13
|
+
return;
|
|
10
14
|
const { targetDir, planPath } = getPlanPath(targetDirArg);
|
|
15
|
+
const agent = resolveAgentId(options.agent);
|
|
11
16
|
return withPlanLock(planPath, async (plan, ctx) => {
|
|
12
17
|
let foundTask = null;
|
|
13
18
|
let foundMilestone = null;
|
|
@@ -25,7 +30,20 @@ export async function runTaskStart(taskId, targetDirArg) {
|
|
|
25
30
|
process.exitCode = 1;
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
33
|
+
const refusal = options.force ? null : startRefusal(plan, foundTask, agent);
|
|
34
|
+
if (refusal) {
|
|
35
|
+
ctx.abort();
|
|
36
|
+
console.error(pc.red(`\n✖ ${refusal}\n`));
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const resumed = foundTask.status === 'in_progress';
|
|
28
41
|
foundTask.status = 'in_progress';
|
|
42
|
+
foundTask.claimedAt = new Date().toISOString();
|
|
43
|
+
if (agent)
|
|
44
|
+
foundTask.claimedBy = agent;
|
|
45
|
+
else if (!resumed)
|
|
46
|
+
delete foundTask.claimedBy;
|
|
29
47
|
if (foundMilestone.status === 'pending') {
|
|
30
48
|
foundMilestone.status = 'in_progress';
|
|
31
49
|
}
|
|
@@ -134,17 +152,20 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
134
152
|
if (failedPhase)
|
|
135
153
|
console.error(pc.yellow(` Phase: ${failedPhase.name}`));
|
|
136
154
|
console.error(pc.yellow(` Command: \`${vResult.command}\``));
|
|
137
|
-
|
|
155
|
+
const digest = digestFailure(targetDir, taskId, vResult);
|
|
156
|
+
if (digest.stderr) {
|
|
138
157
|
console.error(pc.red('\n--- stderr ---'));
|
|
139
|
-
console.error(pc.red(
|
|
158
|
+
console.error(pc.red(digest.stderr));
|
|
140
159
|
}
|
|
141
|
-
if (
|
|
160
|
+
if (digest.stdout) {
|
|
142
161
|
console.error(pc.dim('\n--- stdout ---'));
|
|
143
|
-
console.error(pc.dim(
|
|
162
|
+
console.error(pc.dim(digest.stdout));
|
|
144
163
|
}
|
|
145
164
|
if (vResult.error && !vResult.stderr?.includes(vResult.error)) {
|
|
146
165
|
console.error(pc.red(`\nError: ${vResult.error}`));
|
|
147
166
|
}
|
|
167
|
+
if (digest.logFile)
|
|
168
|
+
console.error(pc.dim(`\n Full output: ${digest.logFile}`));
|
|
148
169
|
console.error(pc.yellow(`\n⚠ Task remains '${foundTask.status}'. Fix the issue and retry:`));
|
|
149
170
|
console.error(pc.white(` nativ task complete ${taskId}`));
|
|
150
171
|
console.error(pc.dim(' (or pass --no-verify to bypass verification check)\n'));
|
|
@@ -189,6 +210,8 @@ export async function runTaskComplete(taskId, targetDirArg, options = {}) {
|
|
|
189
210
|
return;
|
|
190
211
|
}
|
|
191
212
|
taskToComplete.status = 'completed';
|
|
213
|
+
delete taskToComplete.claimedBy;
|
|
214
|
+
delete taskToComplete.claimedAt;
|
|
192
215
|
if (options.notes) {
|
|
193
216
|
taskToComplete.notes = options.notes;
|
|
194
217
|
}
|
|
@@ -246,6 +269,8 @@ export async function runTaskBlock(taskId, reason, targetDirArg) {
|
|
|
246
269
|
}
|
|
247
270
|
foundTask.status = 'blocked';
|
|
248
271
|
foundTask.notes = reason.trim();
|
|
272
|
+
delete foundTask.claimedBy;
|
|
273
|
+
delete foundTask.claimedAt;
|
|
249
274
|
console.log(pc.yellow(`\n⚠ Task [${pc.bold(taskId)}] marked as `) + pc.red('✖ blocked'));
|
|
250
275
|
console.log(pc.dim(` Reason: ${foundTask.notes}\n`));
|
|
251
276
|
});
|
|
@@ -4,6 +4,8 @@ export declare function runTaskList(targetDirArg?: string, options?: {
|
|
|
4
4
|
milestone?: string;
|
|
5
5
|
fastPath?: boolean;
|
|
6
6
|
json?: boolean;
|
|
7
|
+
/** JSON only: id, title, status, role, dependencies and files; leaves out long prose fields. */
|
|
8
|
+
brief?: boolean;
|
|
7
9
|
}): Promise<void>;
|
|
8
10
|
export declare function runTaskNext(targetDirArg?: string, options?: {
|
|
9
11
|
json?: boolean;
|
|
@@ -48,15 +48,17 @@ export async function runTaskList(targetDirArg, options = {}) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
if (options.json) {
|
|
51
|
-
const jsonOutput = results.map(({ task, milestoneId, milestoneName, isAvailable }) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
const jsonOutput = results.map(({ task, milestoneId, milestoneName, isAvailable }) => options.brief
|
|
52
|
+
? briefTask(task, milestoneId, isAvailable)
|
|
53
|
+
: {
|
|
54
|
+
...task,
|
|
55
|
+
milestoneId,
|
|
56
|
+
milestoneName,
|
|
57
|
+
isAvailable,
|
|
58
|
+
roleGuide: getRoleGuide(task.assignedSubagent),
|
|
59
|
+
contractSlice: getRecommendedContractSlice(task.assignedSubagent),
|
|
60
|
+
});
|
|
61
|
+
console.log(JSON.stringify(jsonOutput));
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
console.log(pc.bold(pc.cyan(`\n📋 nativ Tasks: ${plan.projectName}`)));
|
|
@@ -108,6 +110,19 @@ export async function runTaskList(targetDirArg, options = {}) {
|
|
|
108
110
|
pc.gray(`${pendingCount} pending`) + pc.dim(', ') +
|
|
109
111
|
pc.red(`${blockedCount} blocked`) + '\n');
|
|
110
112
|
}
|
|
113
|
+
function briefTask(t, milestoneId, isAvailable) {
|
|
114
|
+
return {
|
|
115
|
+
id: t.id,
|
|
116
|
+
title: t.title,
|
|
117
|
+
status: t.status,
|
|
118
|
+
role: t.assignedSubagent,
|
|
119
|
+
milestoneId,
|
|
120
|
+
isAvailable,
|
|
121
|
+
...(t.dependencies?.length ? { dependencies: t.dependencies } : {}),
|
|
122
|
+
...(t.targetFiles?.length ? { targetFiles: t.targetFiles } : {}),
|
|
123
|
+
...(t.status === 'blocked' && t.notes ? { notes: t.notes } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
111
126
|
/** What a worker needs from a lesson; status and audit fields stay in .ai/learnings.json. */
|
|
112
127
|
function toTaskLearning({ id, insight, details, files }) {
|
|
113
128
|
return { id, insight, ...(details ? { details } : {}), ...(files ? { files } : {}) };
|
|
@@ -168,7 +183,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
168
183
|
}
|
|
169
184
|
if (!activeMilestone) {
|
|
170
185
|
if (options.json) {
|
|
171
|
-
console.log(JSON.stringify({ status: 'all_milestones_completed', activeTask: null }
|
|
186
|
+
console.log(JSON.stringify({ status: 'all_milestones_completed', activeTask: null }));
|
|
172
187
|
}
|
|
173
188
|
else {
|
|
174
189
|
console.log(pc.bold(pc.green('\n🎉 All milestones and tasks are completed!\n')));
|
|
@@ -196,7 +211,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
196
211
|
milestoneId: activeMilestone.id,
|
|
197
212
|
milestoneName: activeMilestone.name,
|
|
198
213
|
activeTask: null,
|
|
199
|
-
}
|
|
214
|
+
}));
|
|
200
215
|
}
|
|
201
216
|
else {
|
|
202
217
|
if (hasBlocked) {
|
|
@@ -233,7 +248,7 @@ export async function runTaskNext(targetDirArg, options = {}) {
|
|
|
233
248
|
...(learnings.length ? { learnings: learnings.map(toTaskLearning) } : {}),
|
|
234
249
|
...(priorEscalations.length ? { priorEscalations } : {}),
|
|
235
250
|
}
|
|
236
|
-
}
|
|
251
|
+
}));
|
|
237
252
|
return;
|
|
238
253
|
}
|
|
239
254
|
const fastTag = targetTask.fastPath ? pc.cyan(' [FAST-PATH PRIORITY]') : '';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface TaskReclaimOptions {
|
|
2
|
+
/** Claims at least this many hours old are abandoned (default 4). */
|
|
3
|
+
olderThan?: number;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
/** Human override: reclaim a named task even though its claim is fresh. */
|
|
7
|
+
force?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ReclaimedTask {
|
|
10
|
+
id: string;
|
|
11
|
+
claimedBy?: string;
|
|
12
|
+
ageHours: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface TaskReclaimResult {
|
|
15
|
+
reclaimed: ReclaimedTask[];
|
|
16
|
+
/** in_progress tasks with no claim time (started before claims existed): reclaim them by id. */
|
|
17
|
+
unknownAge: string[];
|
|
18
|
+
}
|
|
19
|
+
/** Puts in_progress tasks whose agent stopped (crash, closed session) back to pending so another agent can take them. */
|
|
20
|
+
export declare function runTaskReclaim(taskId?: string, targetDirArg?: string, options?: TaskReclaimOptions): Promise<TaskReclaimResult | null>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { withPlanLock } from '../../core/lock-manager.js';
|
|
4
|
+
import { refuseHeadless } from '../../core/human-gate.js';
|
|
5
|
+
import { DEFAULT_STALE_HOURS, claimAgeHours } from '../../core/task-claims.js';
|
|
6
|
+
import { getPlanPath } from './task-common.js';
|
|
7
|
+
const round = (h) => (h === null ? null : Math.round(h * 10) / 10);
|
|
8
|
+
/** Puts in_progress tasks whose agent stopped (crash, closed session) back to pending so another agent can take them. */
|
|
9
|
+
export async function runTaskReclaim(taskId, targetDirArg, options = {}) {
|
|
10
|
+
const hours = options.olderThan ?? DEFAULT_STALE_HOURS;
|
|
11
|
+
const fail = (message) => {
|
|
12
|
+
if (options.json)
|
|
13
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
14
|
+
else
|
|
15
|
+
console.error(pc.red(`\n✖ ${message}\n`));
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
if (!Number.isFinite(hours) || hours <= 0)
|
|
20
|
+
return fail('--older-than must be a positive number of hours');
|
|
21
|
+
if (options.force && refuseHeadless('nativ task reclaim --force'))
|
|
22
|
+
return null;
|
|
23
|
+
// `nativ task reclaim <dir>` reads naturally, so a lone existing directory is the target, not a task id.
|
|
24
|
+
if (taskId && !targetDirArg && fs.existsSync(taskId) && fs.statSync(taskId).isDirectory()) {
|
|
25
|
+
targetDirArg = taskId;
|
|
26
|
+
taskId = undefined;
|
|
27
|
+
}
|
|
28
|
+
const { planPath } = getPlanPath(targetDirArg);
|
|
29
|
+
const result = { reclaimed: [], unknownAge: [] };
|
|
30
|
+
let error = null;
|
|
31
|
+
let planRead = false;
|
|
32
|
+
await withPlanLock(planPath, (plan, ctx) => {
|
|
33
|
+
planRead = true;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const inProgress = plan.milestones.flatMap((m) => m.tasks).filter((t) => t.status === 'in_progress');
|
|
36
|
+
if (taskId && !inProgress.some((t) => t.id === taskId)) {
|
|
37
|
+
error = `Task [${taskId}] is not in_progress, so there is nothing to reclaim.`;
|
|
38
|
+
}
|
|
39
|
+
for (const t of inProgress) {
|
|
40
|
+
if (error || (taskId && t.id !== taskId))
|
|
41
|
+
continue;
|
|
42
|
+
const age = claimAgeHours(t, now);
|
|
43
|
+
const stale = age !== null && age >= hours;
|
|
44
|
+
if (!taskId && age === null) {
|
|
45
|
+
result.unknownAge.push(t.id);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!stale && !(taskId && age === null) && !options.force) {
|
|
49
|
+
if (taskId)
|
|
50
|
+
error = `Task [${t.id}] was claimed${t.claimedBy ? ` by ${t.claimedBy}` : ''} ${round(age)}h ago; its agent may still be working. A human can pass --force.`;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
result.reclaimed.push({ id: t.id, ...(t.claimedBy ? { claimedBy: t.claimedBy } : {}), ageHours: round(age) });
|
|
54
|
+
if (options.dryRun)
|
|
55
|
+
continue;
|
|
56
|
+
t.status = 'pending';
|
|
57
|
+
delete t.claimedBy;
|
|
58
|
+
delete t.claimedAt;
|
|
59
|
+
}
|
|
60
|
+
if (error || options.dryRun || !result.reclaimed.length)
|
|
61
|
+
ctx.abort();
|
|
62
|
+
});
|
|
63
|
+
if (!planRead)
|
|
64
|
+
return fail(`No readable plan at ${planPath}`);
|
|
65
|
+
if (error)
|
|
66
|
+
return fail(error);
|
|
67
|
+
if (options.json) {
|
|
68
|
+
console.log(JSON.stringify({ ok: true, dryRun: Boolean(options.dryRun), olderThanHours: hours, ...result }));
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
const verb = options.dryRun ? 'Would reclaim' : 'Reclaimed';
|
|
72
|
+
if (!result.reclaimed.length)
|
|
73
|
+
console.log(pc.dim(`\nNo in_progress task has a claim older than ${hours}h.`));
|
|
74
|
+
for (const r of result.reclaimed) {
|
|
75
|
+
const who = r.claimedBy ? ` from ${r.claimedBy}` : '';
|
|
76
|
+
const age = r.ageHours === null ? '' : ` (${r.ageHours}h old)`;
|
|
77
|
+
console.log(pc.yellow(`\n↺ ${verb} [${pc.bold(r.id)}]${who}${age}: back to pending.`));
|
|
78
|
+
}
|
|
79
|
+
if (result.reclaimed.length && !options.dryRun) {
|
|
80
|
+
console.log(pc.dim(' The previous agent may have left partial changes in the working tree; review them before restarting.'));
|
|
81
|
+
}
|
|
82
|
+
if (result.unknownAge.length) {
|
|
83
|
+
console.log(pc.dim(` No claim time on: ${result.unknownAge.join(', ')}. Reclaim one by id: nativ task reclaim <taskId>`));
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
return result;
|
|
87
|
+
}
|
package/dist/commands/task.d.ts
CHANGED
|
@@ -7,5 +7,7 @@ export { getRoleGuide, getRecommendedContractSlice, } from './task/task-common.j
|
|
|
7
7
|
export type { TaskUnlockOptions, TaskCompleteOptions, TaskAddOptions, TaskAddResult, ProposePatchOptions, } from './task/task-common.js';
|
|
8
8
|
export { runTaskList, runTaskNext } from './task/task-query.js';
|
|
9
9
|
export { runTaskStart, runTaskComplete, runTaskBlock, runTaskUnlock, } from './task/task-lifecycle.js';
|
|
10
|
+
export { runTaskReclaim } from './task/task-reclaim.js';
|
|
11
|
+
export type { TaskReclaimOptions, TaskReclaimResult } from './task/task-reclaim.js';
|
|
10
12
|
export { FAST_PATH_MILESTONE_NAME, generateNextTaskId, generateNextMilestoneId, runTaskAdd, } from './task/task-add.js';
|
|
11
13
|
export { runTaskEscalate, runTaskProposePatch, } from './task/task-escalate.js';
|
package/dist/commands/task.js
CHANGED
|
@@ -6,5 +6,6 @@ export { loadPlan, savePlan, withPlanLock } from '../core/lock-manager.js';
|
|
|
6
6
|
export { getRoleGuide, getRecommendedContractSlice, } from './task/task-common.js';
|
|
7
7
|
export { runTaskList, runTaskNext } from './task/task-query.js';
|
|
8
8
|
export { runTaskStart, runTaskComplete, runTaskBlock, runTaskUnlock, } from './task/task-lifecycle.js';
|
|
9
|
+
export { runTaskReclaim } from './task/task-reclaim.js';
|
|
9
10
|
export { FAST_PATH_MILESTONE_NAME, generateNextTaskId, generateNextMilestoneId, runTaskAdd, } from './task/task-add.js';
|
|
10
11
|
export { runTaskEscalate, runTaskProposePatch, } from './task/task-escalate.js';
|
package/dist/commands/triage.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function runTriage(escalationIdArg, targetDirArg, options = {}) {
|
|
|
18
18
|
};
|
|
19
19
|
const fail = (message) => {
|
|
20
20
|
if (options.json)
|
|
21
|
-
print(JSON.stringify({ ok: false, error: message }
|
|
21
|
+
print(JSON.stringify({ ok: false, error: message }));
|
|
22
22
|
else
|
|
23
23
|
print(pc.red(`\nTriage failed: ${message}\n`));
|
|
24
24
|
process.exitCode = 1;
|
|
@@ -76,7 +76,7 @@ export async function runTriage(escalationIdArg, targetDirArg, options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
if (!targets.length) {
|
|
78
78
|
if (options.json)
|
|
79
|
-
print(JSON.stringify({ ok: true, evaluations: [] }
|
|
79
|
+
print(JSON.stringify({ ok: true, evaluations: [] }));
|
|
80
80
|
else if (!pending.length)
|
|
81
81
|
print(pc.green('\nNo pending escalations. Nothing to triage.\n'));
|
|
82
82
|
return [];
|
|
@@ -119,7 +119,7 @@ export async function runTriage(escalationIdArg, targetDirArg, options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
if (options.json) {
|
|
122
|
-
print(JSON.stringify({ ok: results.every((r) => r.evaluation.ok), evaluations: results }
|
|
122
|
+
print(JSON.stringify({ ok: results.every((r) => r.evaluation.ok), evaluations: results }));
|
|
123
123
|
}
|
|
124
124
|
else {
|
|
125
125
|
const evaluated = results.filter((r) => r.evaluation.ok).map((r) => r.evaluation);
|
package/dist/commands/update.js
CHANGED
|
@@ -1,10 +1,54 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import pc from 'picocolors';
|
|
4
5
|
import { detectProject } from '../scanner/detector.js';
|
|
5
6
|
import { applySetup, getTemplatesDir, planTemplateFile } from '../core/setup-assets.js';
|
|
6
7
|
import { printSetupResult } from './setup.js';
|
|
7
8
|
import { runValidate } from './validate.js';
|
|
9
|
+
const noCr = (text) => text.replace(/\r/g, '');
|
|
10
|
+
function walkFiles(dir) {
|
|
11
|
+
if (!fs.existsSync(dir))
|
|
12
|
+
return [];
|
|
13
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
14
|
+
.flatMap((e) => (e.isDirectory() ? walkFiles(path.join(dir, e.name)) : [path.join(dir, e.name)]));
|
|
15
|
+
}
|
|
16
|
+
// Plain copies (no stamp) so doctor can compare them; edited files are kept unless forced.
|
|
17
|
+
function syncTasteBundle(templatesDir, aiDir, force) {
|
|
18
|
+
const src = path.join(templatesDir, 'design', 'taste');
|
|
19
|
+
let kept = 0;
|
|
20
|
+
const manifestPath = path.join(aiDir, 'design', 'taste', '.manifest.json');
|
|
21
|
+
let manifest = {};
|
|
22
|
+
try {
|
|
23
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch { /* installed before manifests */ }
|
|
26
|
+
const hash = (t) => createHash('sha256').update(noCr(t)).digest('hex');
|
|
27
|
+
for (const file of walkFiles(src)) {
|
|
28
|
+
const rel = path.relative(src, file).split(path.sep).join('/');
|
|
29
|
+
const dest = path.join(aiDir, 'design', 'taste', ...rel.split('/'));
|
|
30
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
31
|
+
const existing = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : null;
|
|
32
|
+
const shown = `.ai/design/taste/${rel}`;
|
|
33
|
+
if (existing !== null && noCr(existing) === noCr(text)) {
|
|
34
|
+
printChange({ path: shown, action: 'unchanged' });
|
|
35
|
+
manifest[rel] ??= hash(text);
|
|
36
|
+
}
|
|
37
|
+
else if (existing !== null && !force && manifest[rel] !== hash(existing)) {
|
|
38
|
+
kept++;
|
|
39
|
+
printChange({ path: shown, action: 'skipped', detail: 'differs from the template' });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
43
|
+
fs.writeFileSync(dest, text, 'utf8');
|
|
44
|
+
manifest[rel] = hash(text);
|
|
45
|
+
printChange({ path: shown, action: existing === null ? 'created' : 'updated' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
49
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
50
|
+
return kept;
|
|
51
|
+
}
|
|
8
52
|
const ICON = { created: '+', updated: '~', unchanged: '=', skipped: '!' };
|
|
9
53
|
function printChange(change) {
|
|
10
54
|
const paint = change.action === 'created' ? pc.green : change.action === 'updated' ? pc.cyan : change.action === 'skipped' ? pc.yellow : pc.dim;
|
|
@@ -62,6 +106,8 @@ export async function runUpdate(targetDirArg, options = {}) {
|
|
|
62
106
|
kept++;
|
|
63
107
|
printChange({ path: item.rel, action: plan.action, detail: plan.detail });
|
|
64
108
|
}
|
|
109
|
+
console.log(pc.bold('\nDesign bundle'));
|
|
110
|
+
kept += syncTasteBundle(templatesDir, aiDir, options.force);
|
|
65
111
|
// 2. Everything `nativ setup` manages: AGENTS.md, .claude/agents/, settings, MCP server
|
|
66
112
|
console.log(pc.bold('\nClaude Code configuration'));
|
|
67
113
|
const setup = applySetup(targetDir, { force: options.force });
|
|
@@ -82,6 +82,7 @@ export async function runValidate(targetDirArg) {
|
|
|
82
82
|
checkFile('.ai/subagents/qa-tester.md');
|
|
83
83
|
console.log(pc.dim(' Specialized Units:'));
|
|
84
84
|
const specializedProfiles = [
|
|
85
|
+
'architect.md',
|
|
85
86
|
'flutter-developer.md',
|
|
86
87
|
'devops-agent.md',
|
|
87
88
|
'security-auditor.md',
|
package/dist/commands/verify.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { verifyBatch } from '../core/verifier.js';
|
|
4
|
+
import { digestFailure } from '../core/verifier/output-digest.js';
|
|
5
|
+
/** Passing runs need no output; failing ones get a digest and the path to the full log. */
|
|
6
|
+
function compactBatch(batch, root) {
|
|
7
|
+
return {
|
|
8
|
+
...batch,
|
|
9
|
+
tasks: batch.tasks.map((t) => {
|
|
10
|
+
if (t.result.success)
|
|
11
|
+
return { ...t, result: { ...t.result, stdout: '', stderr: '' } };
|
|
12
|
+
const { stdout, stderr, logFile } = digestFailure(root, t.taskId, t.result);
|
|
13
|
+
return { ...t, result: { ...t.result, stdout, stderr, logFile } };
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
4
17
|
export async function runVerify(taskId, targetDirArg, options = {}) {
|
|
5
18
|
const targetDir = path.resolve(targetDirArg || process.cwd());
|
|
6
19
|
const batch = await verifyBatch(targetDir, {
|
|
@@ -10,7 +23,7 @@ export async function runVerify(taskId, targetDirArg, options = {}) {
|
|
|
10
23
|
timeout: options.timeout,
|
|
11
24
|
});
|
|
12
25
|
if (options.json) {
|
|
13
|
-
console.log(JSON.stringify(batch,
|
|
26
|
+
console.log(JSON.stringify(compactBatch(batch, targetDir)));
|
|
14
27
|
if (batch.failed > 0) {
|
|
15
28
|
process.exitCode = 1;
|
|
16
29
|
}
|
|
@@ -43,17 +56,20 @@ export async function runVerify(taskId, targetDirArg, options = {}) {
|
|
|
43
56
|
const failedPhase = t.result.phases?.find((p) => !p.success);
|
|
44
57
|
if (failedPhase)
|
|
45
58
|
console.log(pc.red(` Failed in phase "${failedPhase.name}"`));
|
|
46
|
-
|
|
59
|
+
const digest = digestFailure(targetDir, t.taskId, t.result);
|
|
60
|
+
if (digest.stderr) {
|
|
47
61
|
console.log(pc.red(`\n--- [${t.taskId}] stderr ---`));
|
|
48
|
-
console.log(pc.red(
|
|
62
|
+
console.log(pc.red(digest.stderr));
|
|
49
63
|
}
|
|
50
|
-
if (
|
|
64
|
+
if (digest.stdout) {
|
|
51
65
|
console.log(pc.dim(`\n--- [${t.taskId}] stdout ---`));
|
|
52
|
-
console.log(pc.dim(
|
|
66
|
+
console.log(pc.dim(digest.stdout));
|
|
53
67
|
}
|
|
54
68
|
if (t.result.error && !t.result.stderr?.includes(t.result.error)) {
|
|
55
69
|
console.log(pc.red(`\nError: ${t.result.error}`));
|
|
56
70
|
}
|
|
71
|
+
if (digest.logFile)
|
|
72
|
+
console.log(pc.dim(` Full output: ${digest.logFile}`));
|
|
57
73
|
console.log('');
|
|
58
74
|
}
|
|
59
75
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const DESIGN_PROFILES: readonly ["taste-v1", "taste-v2"];
|
|
2
|
+
export type DesignProfile = (typeof DESIGN_PROFILES)[number];
|
|
3
|
+
export interface DesignFinding {
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
rule: string;
|
|
7
|
+
severity: 'error' | 'warning';
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
interface Rule {
|
|
11
|
+
id: string;
|
|
12
|
+
profiles: readonly DesignProfile[];
|
|
13
|
+
severity: 'error' | 'warning';
|
|
14
|
+
message: string;
|
|
15
|
+
pattern?: RegExp;
|
|
16
|
+
exts?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare const DESIGN_RULES: readonly Rule[];
|
|
19
|
+
export declare const SCAN_EXTENSIONS: string[];
|
|
20
|
+
export declare function readProfile(uiSpecsText: string): DesignProfile | null;
|
|
21
|
+
export declare function isCommentLine(line: string): boolean;
|
|
22
|
+
export declare function checkText(text: string, ext: string, profile: DesignProfile): DesignFinding[];
|
|
23
|
+
export {};
|