@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +100 -5
  3. package/dist/commands/design-check.d.ts +4 -0
  4. package/dist/commands/design-check.js +76 -0
  5. package/dist/commands/init.d.ts +1 -0
  6. package/dist/commands/init.js +55 -11
  7. package/dist/commands/notify.d.ts +2 -0
  8. package/dist/commands/notify.js +25 -0
  9. package/dist/commands/session.d.ts +22 -0
  10. package/dist/commands/session.js +106 -0
  11. package/dist/commands/task/task-add.js +1 -1
  12. package/dist/commands/task/task-common.js +6 -6
  13. package/dist/commands/task/task-escalate.js +8 -10
  14. package/dist/commands/task/task-lifecycle.d.ts +7 -1
  15. package/dist/commands/task/task-lifecycle.js +30 -5
  16. package/dist/commands/task/task-query.d.ts +2 -0
  17. package/dist/commands/task/task-query.js +27 -12
  18. package/dist/commands/task/task-reclaim.d.ts +20 -0
  19. package/dist/commands/task/task-reclaim.js +87 -0
  20. package/dist/commands/task.d.ts +2 -0
  21. package/dist/commands/task.js +1 -0
  22. package/dist/commands/triage.js +3 -3
  23. package/dist/commands/update.js +46 -0
  24. package/dist/commands/validate.js +1 -0
  25. package/dist/commands/verify.js +21 -5
  26. package/dist/core/design-rules.d.ts +23 -0
  27. package/dist/core/design-rules.js +115 -0
  28. package/dist/core/enforcement.js +18 -17
  29. package/dist/core/harness-integrity.js +26 -7
  30. package/dist/core/lock-manager.d.ts +6 -0
  31. package/dist/core/lock-manager.js +50 -41
  32. package/dist/core/notify.d.ts +38 -0
  33. package/dist/core/notify.js +133 -0
  34. package/dist/core/profiles.js +7 -16
  35. package/dist/core/session-memory.d.ts +42 -0
  36. package/dist/core/session-memory.js +121 -0
  37. package/dist/core/setup/pm-playbook.d.ts +5 -0
  38. package/dist/core/setup/pm-playbook.js +26 -0
  39. package/dist/core/setup/session-context.js +35 -2
  40. package/dist/core/setup/setup-planner.js +2 -2
  41. package/dist/core/setup/types.d.ts +2 -0
  42. package/dist/core/setup/types.js +5 -1
  43. package/dist/core/task-claims.d.ts +15 -0
  44. package/dist/core/task-claims.js +32 -0
  45. package/dist/core/tier1-liaison.d.ts +1 -0
  46. package/dist/core/tier1-liaison.js +25 -27
  47. package/dist/core/verifier/output-digest.d.ts +19 -0
  48. package/dist/core/verifier/output-digest.js +48 -0
  49. package/dist/db/data-engine.js +33 -32
  50. package/dist/db/introspector.js +61 -60
  51. package/dist/db/sqlite.d.ts +3 -0
  52. package/dist/db/sqlite.js +10 -0
  53. package/dist/governor/circuit-breaker.js +12 -0
  54. package/dist/governor/patch-applier.d.ts +1 -0
  55. package/dist/governor/patch-applier.js +3 -7
  56. package/dist/index.js +40 -6
  57. package/dist/mcp/server.d.ts +1 -1
  58. package/dist/mcp/server.js +53 -14
  59. package/dist/providers/claude-api.js +3 -0
  60. package/dist/providers/gemini.js +4 -1
  61. package/dist/providers/registry.d.ts +6 -2
  62. package/dist/providers/registry.js +28 -16
  63. package/dist/providers/types.d.ts +4 -1
  64. package/dist/runner/agent-supervisor.js +2 -2
  65. package/dist/runner/cli-config.js +1 -1
  66. package/dist/scanner/types.d.ts +5 -1
  67. package/dist/scanner/types.js +7 -0
  68. package/dist/server/routes/task-routes.d.ts +2 -0
  69. package/dist/server/routes/triage-routes.js +3 -18
  70. package/dist/server/studio-ui/pages/benchmarks/styles.js +8 -5
  71. package/dist/server/studio-ui/pages/database/browser.js +1 -1
  72. package/dist/server/studio-ui/pages/database/connection.js +1 -1
  73. package/dist/server/studio-ui/pages/database/data-shell.js +5 -5
  74. package/dist/server/studio-ui/pages/database/drift.js +1 -1
  75. package/dist/server/studio-ui/pages/database/explorer.js +2 -2
  76. package/dist/server/studio-ui/pages/database/styles.js +5 -5
  77. package/dist/server/studio-ui/pages/flow/styles.js +3 -3
  78. package/dist/server/studio-ui/pages/home/script.js +110 -91
  79. package/dist/server/studio-ui/pages/home/styles.js +33 -45
  80. package/dist/server/studio-ui/pages/tasks/script.js +150 -3
  81. package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
  82. package/dist/server/studio-ui/pages/team/script.js +49 -52
  83. package/dist/server/studio-ui/pages/team/styles.js +21 -20
  84. package/dist/server/studio-ui/pages/worktrees/script.js +47 -35
  85. package/dist/server/studio-ui/pages/worktrees/styles.js +12 -10
  86. package/dist/server/studio-ui/shell/brand.d.ts +4 -0
  87. package/dist/server/studio-ui/shell/brand.js +16 -0
  88. package/dist/server/studio-ui/shell/console.js +61 -2
  89. package/dist/server/studio-ui/shell/core.js +8 -8
  90. package/dist/server/studio-ui/shell/detail-view.js +1 -1
  91. package/dist/server/studio-ui/shell/dispatch.js +18 -0
  92. package/dist/server/studio-ui/shell/events-tools.js +5 -1
  93. package/dist/server/studio-ui/shell/format.js +5 -6
  94. package/dist/server/studio-ui/shell/icons.js +23 -15
  95. package/dist/server/studio-ui/shell/markup-head.js +11 -6
  96. package/dist/server/studio-ui/shell/markup-tools.js +19 -10
  97. package/dist/server/studio-ui/shell/paint.js +3 -0
  98. package/dist/server/studio-ui/shell/pipeline-state.js +1 -1
  99. package/dist/server/studio-ui/shell/proposals.js +6 -2
  100. package/dist/server/studio-ui/shell/pulse.d.ts +1 -0
  101. package/dist/server/studio-ui/shell/pulse.js +67 -0
  102. package/dist/server/studio-ui/shell/router.js +25 -0
  103. package/dist/server/studio-ui/shell/sidebar-view.js +6 -3
  104. package/dist/server/studio-ui/shell/toasts-format.js +4 -4
  105. package/dist/server/studio-ui/styles/console.js +33 -13
  106. package/dist/server/studio-ui/styles/dialogs.js +4 -4
  107. package/dist/server/studio-ui/styles/responsive.js +8 -0
  108. package/dist/server/studio-ui/styles/shell.js +24 -5
  109. package/dist/server/studio-ui/styles/strategist.js +10 -10
  110. package/dist/server/studio-ui/styles/tokens-base.js +23 -23
  111. package/dist/server/studio-ui.js +2 -0
  112. package/package.json +2 -2
  113. package/plugin/.claude-plugin/plugin.json +1 -1
  114. package/plugin/agents/architect.md +11 -12
  115. package/plugin/agents/explorer.md +1 -1
  116. package/plugin/agents/verifier.md +2 -2
  117. package/plugin/agents/worker.md +5 -7
  118. package/plugin/skills/nativ/SKILL.md +36 -49
  119. package/templates/AGENTS.md +36 -49
  120. package/templates/CLAUDE.md +1 -41
  121. package/templates/GEMINI.md +3 -2
  122. package/templates/claude-agents/architect.md +11 -12
  123. package/templates/claude-agents/explorer.md +1 -1
  124. package/templates/claude-agents/verifier.md +2 -2
  125. package/templates/claude-agents/worker.md +5 -7
  126. package/templates/design/taste/LICENSE +21 -0
  127. package/templates/design/taste/SOURCE.md +28 -0
  128. package/templates/design/taste/v1/DIGEST.md +57 -0
  129. package/templates/design/taste/v1/SKILL.md +226 -0
  130. package/templates/design/taste/v2/DIGEST.md +62 -0
  131. package/templates/design/taste/v2/SKILL.md +1206 -0
  132. package/templates/dot-ai/api_contracts.json +66 -66
  133. package/templates/dot-ai/context.md +38 -38
  134. package/templates/dot-ai/db_schema.json +39 -39
  135. package/templates/dot-ai/subagents/architect.md +6 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +2 -11
  137. package/templates/dot-ai/subagents/frontend.md +47 -44
  138. 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 (UI tokens & hierarchy) + .ai/api_contracts.json (Client endpoint integration)';
27
+ return '.ai/ui_specs.md + .ai/api_contracts.json';
28
28
  }
29
29
  if (sub.includes('backend')) {
30
- return '.ai/api_contracts.json (Endpoint routes, request/response schemas) + .ai/db_schema.json (Models)';
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 (Database schema contract & models)';
33
+ return '.ai/db_schema.json';
34
34
  }
35
35
  if (sub.includes('devops')) {
36
- return '.ai/context.md (Infrastructure, runtime & deployment guardrails)';
36
+ return '.ai/context.md';
37
37
  }
38
38
  if (sub.includes('security')) {
39
- return 'Target files & dependency manifests (.ai/context.md security guardrails)';
39
+ return 'targetFiles + dependency manifests + .ai/context.md';
40
40
  }
41
41
  if (sub.includes('qa')) {
42
- return 'Target files, .ai/api_contracts.json, and verification test suite';
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 declare function runTaskStart(taskId: string, targetDirArg?: string): Promise<void | null>;
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
- if (vResult.stderr && vResult.stderr.trim()) {
155
+ const digest = digestFailure(targetDir, taskId, vResult);
156
+ if (digest.stderr) {
138
157
  console.error(pc.red('\n--- stderr ---'));
139
- console.error(pc.red(vResult.stderr.trim()));
158
+ console.error(pc.red(digest.stderr));
140
159
  }
141
- if (vResult.stdout && vResult.stdout.trim() && !vResult.stderr?.trim()) {
160
+ if (digest.stdout) {
142
161
  console.error(pc.dim('\n--- stdout ---'));
143
- console.error(pc.dim(vResult.stdout.trim()));
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
- ...task,
53
- milestoneId,
54
- milestoneName,
55
- isAvailable,
56
- roleGuide: getRoleGuide(task.assignedSubagent),
57
- contractSlice: getRecommendedContractSlice(task.assignedSubagent),
58
- }));
59
- console.log(JSON.stringify(jsonOutput, null, 2));
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 }, null, 2));
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
- }, null, 2));
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
- }, null, 2));
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
+ }
@@ -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';
@@ -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';
@@ -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 }, null, 2));
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: [] }, null, 2));
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 }, null, 2));
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);
@@ -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',
@@ -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, null, 2));
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
- if (t.result.stderr && t.result.stderr.trim()) {
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(t.result.stderr.trim()));
62
+ console.log(pc.red(digest.stderr));
49
63
  }
50
- if (t.result.stdout && t.result.stdout.trim() && !t.result.stderr?.trim()) {
64
+ if (digest.stdout) {
51
65
  console.log(pc.dim(`\n--- [${t.taskId}] stdout ---`));
52
- console.log(pc.dim(t.result.stdout.trim()));
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 {};