@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
@@ -1,6 +1,7 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import { isNoSqlEngine } from './types.js';
3
3
  import { databaseNameFromUrl, detectEngine, maskConnectionString, MASK, parseFirestoreUrl, redactMediaUrl, sqlitePathFromUrl, } from './env-parser.js';
4
+ import { loadSqlite } from './sqlite.js';
4
5
  /**
5
6
  * Multi-engine schema introspector.
6
7
  * Reads structural metadata only (tables, columns, types, nullability, keys, indexes).
@@ -81,30 +82,30 @@ async function createPostgresDriver(url) {
81
82
  },
82
83
  async tables() {
83
84
  const builder = new TableBuilder();
84
- const tables = await client.query(`
85
- SELECT t.table_name, obj_description(format('%I.%I', t.table_schema, t.table_name)::regclass, 'pg_class') AS description
86
- FROM information_schema.tables t
87
- WHERE t.table_schema = current_schema() AND t.table_type = 'BASE TABLE'
85
+ const tables = await client.query(`
86
+ SELECT t.table_name, obj_description(format('%I.%I', t.table_schema, t.table_name)::regclass, 'pg_class') AS description
87
+ FROM information_schema.tables t
88
+ WHERE t.table_schema = current_schema() AND t.table_type = 'BASE TABLE'
88
89
  ORDER BY t.table_name`);
89
90
  for (const row of tables.rows) {
90
91
  const table = builder.get(row.table_name);
91
92
  if (row.description)
92
93
  table.description = row.description;
93
94
  }
94
- const pks = await client.query(`
95
- SELECT kcu.table_name, kcu.column_name
96
- FROM information_schema.table_constraints tc
97
- JOIN information_schema.key_column_usage kcu
98
- ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name
95
+ const pks = await client.query(`
96
+ SELECT kcu.table_name, kcu.column_name
97
+ FROM information_schema.table_constraints tc
98
+ JOIN information_schema.key_column_usage kcu
99
+ ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name
99
100
  WHERE tc.table_schema = current_schema() AND tc.constraint_type = 'PRIMARY KEY'`);
100
101
  const pkSet = new Set(pks.rows.map((r) => `${r.table_name}.${r.column_name}`));
101
- const columns = await client.query(`
102
- SELECT c.table_name, c.column_name, c.data_type, c.udt_name, c.character_maximum_length,
103
- c.numeric_precision, c.numeric_scale, c.is_nullable, c.column_default
104
- FROM information_schema.columns c
105
- JOIN information_schema.tables t
106
- ON t.table_schema = c.table_schema AND t.table_name = c.table_name AND t.table_type = 'BASE TABLE'
107
- WHERE c.table_schema = current_schema()
102
+ const columns = await client.query(`
103
+ SELECT c.table_name, c.column_name, c.data_type, c.udt_name, c.character_maximum_length,
104
+ c.numeric_precision, c.numeric_scale, c.is_nullable, c.column_default
105
+ FROM information_schema.columns c
106
+ JOIN information_schema.tables t
107
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name AND t.table_type = 'BASE TABLE'
108
+ WHERE c.table_schema = current_schema()
108
109
  ORDER BY c.table_name, c.ordinal_position`);
109
110
  for (const row of columns.rows) {
110
111
  builder.get(row.table_name).columns.push({
@@ -116,31 +117,31 @@ async function createPostgresDriver(url) {
116
117
  });
117
118
  }
118
119
  // information_schema has no index view — pg_catalog is the only source.
119
- const indexes = await client.query(`
120
- SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,
121
- (SELECT array_agg(a.attname::text ORDER BY k.ord)
122
- FROM unnest(ix.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord)
123
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum) AS columns
124
- FROM pg_index ix
125
- JOIN pg_class t ON t.oid = ix.indrelid
126
- JOIN pg_class i ON i.oid = ix.indexrelid
127
- JOIN pg_namespace n ON n.oid = t.relnamespace
128
- WHERE n.nspname = current_schema() AND NOT ix.indisprimary AND t.relkind IN ('r', 'p')
120
+ const indexes = await client.query(`
121
+ SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,
122
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
123
+ FROM unnest(ix.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord)
124
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum) AS columns
125
+ FROM pg_index ix
126
+ JOIN pg_class t ON t.oid = ix.indrelid
127
+ JOIN pg_class i ON i.oid = ix.indexrelid
128
+ JOIN pg_namespace n ON n.oid = t.relnamespace
129
+ WHERE n.nspname = current_schema() AND NOT ix.indisprimary AND t.relkind IN ('r', 'p')
129
130
  ORDER BY t.relname, i.relname`);
130
131
  for (const row of indexes.rows) {
131
132
  builder.get(row.table_name).indexes.push({ name: row.index_name, columns: row.columns ?? [], unique: row.is_unique });
132
133
  }
133
- const fks = await client.query(`
134
- SELECT con.conname AS constraint_name, cl.relname AS table_name, a.attname AS column_name,
135
- fcl.relname AS ref_table, fa.attname AS ref_column, con.confdeltype AS on_delete
136
- FROM pg_constraint con
137
- JOIN pg_class cl ON cl.oid = con.conrelid
138
- JOIN pg_namespace n ON n.oid = cl.relnamespace
139
- JOIN pg_class fcl ON fcl.oid = con.confrelid
140
- CROSS JOIN LATERAL unnest(con.conkey, con.confkey) AS k(attnum, fattnum)
141
- JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = k.attnum
142
- JOIN pg_attribute fa ON fa.attrelid = con.confrelid AND fa.attnum = k.fattnum
143
- WHERE n.nspname = current_schema() AND con.contype = 'f'
134
+ const fks = await client.query(`
135
+ SELECT con.conname AS constraint_name, cl.relname AS table_name, a.attname AS column_name,
136
+ fcl.relname AS ref_table, fa.attname AS ref_column, con.confdeltype AS on_delete
137
+ FROM pg_constraint con
138
+ JOIN pg_class cl ON cl.oid = con.conrelid
139
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
140
+ JOIN pg_class fcl ON fcl.oid = con.confrelid
141
+ CROSS JOIN LATERAL unnest(con.conkey, con.confkey) AS k(attnum, fattnum)
142
+ JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = k.attnum
143
+ JOIN pg_attribute fa ON fa.attrelid = con.confrelid AND fa.attnum = k.fattnum
144
+ WHERE n.nspname = current_schema() AND con.contype = 'f'
144
145
  ORDER BY cl.relname, con.conname`);
145
146
  for (const row of fks.rows) {
146
147
  builder.get(row.table_name).foreignKeys.push({
@@ -177,22 +178,22 @@ async function createMysqlDriver(url) {
177
178
  },
178
179
  async tables() {
179
180
  const builder = new TableBuilder();
180
- for (const row of await rows(`
181
- SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment
182
- FROM information_schema.TABLES
183
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'
181
+ for (const row of await rows(`
182
+ SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment
183
+ FROM information_schema.TABLES
184
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'
184
185
  ORDER BY TABLE_NAME`)) {
185
186
  const table = builder.get(row.table_name);
186
187
  if (row.table_comment)
187
188
  table.description = row.table_comment;
188
189
  }
189
- for (const row of await rows(`
190
- SELECT c.TABLE_NAME AS table_name, c.COLUMN_NAME AS column_name, c.COLUMN_TYPE AS column_type,
191
- c.IS_NULLABLE AS is_nullable, c.COLUMN_DEFAULT AS column_default, c.COLUMN_KEY AS column_key
192
- FROM information_schema.COLUMNS c
193
- JOIN information_schema.TABLES t
194
- ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME AND t.TABLE_TYPE = 'BASE TABLE'
195
- WHERE c.TABLE_SCHEMA = DATABASE()
190
+ for (const row of await rows(`
191
+ SELECT c.TABLE_NAME AS table_name, c.COLUMN_NAME AS column_name, c.COLUMN_TYPE AS column_type,
192
+ c.IS_NULLABLE AS is_nullable, c.COLUMN_DEFAULT AS column_default, c.COLUMN_KEY AS column_key
193
+ FROM information_schema.COLUMNS c
194
+ JOIN information_schema.TABLES t
195
+ ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME AND t.TABLE_TYPE = 'BASE TABLE'
196
+ WHERE c.TABLE_SCHEMA = DATABASE()
196
197
  ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`)) {
197
198
  builder.get(row.table_name).columns.push({
198
199
  name: row.column_name,
@@ -203,10 +204,10 @@ async function createMysqlDriver(url) {
203
204
  });
204
205
  }
205
206
  const indexMap = new Map();
206
- for (const row of await rows(`
207
- SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique, COLUMN_NAME AS column_name
208
- FROM information_schema.STATISTICS
209
- WHERE TABLE_SCHEMA = DATABASE() AND INDEX_NAME <> 'PRIMARY'
207
+ for (const row of await rows(`
208
+ SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique, COLUMN_NAME AS column_name
209
+ FROM information_schema.STATISTICS
210
+ WHERE TABLE_SCHEMA = DATABASE() AND INDEX_NAME <> 'PRIMARY'
210
211
  ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`)) {
211
212
  const key = `${row.table_name}.${row.index_name}`;
212
213
  let entry = indexMap.get(key);
@@ -219,13 +220,13 @@ async function createMysqlDriver(url) {
219
220
  }
220
221
  for (const { table, index } of indexMap.values())
221
222
  builder.get(table).indexes.push(index);
222
- for (const row of await rows(`
223
- SELECT k.CONSTRAINT_NAME AS constraint_name, k.TABLE_NAME AS table_name, k.COLUMN_NAME AS column_name,
224
- k.REFERENCED_TABLE_NAME AS ref_table, k.REFERENCED_COLUMN_NAME AS ref_column, r.DELETE_RULE AS delete_rule
225
- FROM information_schema.KEY_COLUMN_USAGE k
226
- JOIN information_schema.REFERENTIAL_CONSTRAINTS r
227
- ON r.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND r.TABLE_NAME = k.TABLE_NAME
228
- WHERE k.TABLE_SCHEMA = DATABASE() AND k.REFERENCED_TABLE_NAME IS NOT NULL
223
+ for (const row of await rows(`
224
+ SELECT k.CONSTRAINT_NAME AS constraint_name, k.TABLE_NAME AS table_name, k.COLUMN_NAME AS column_name,
225
+ k.REFERENCED_TABLE_NAME AS ref_table, k.REFERENCED_COLUMN_NAME AS ref_column, r.DELETE_RULE AS delete_rule
226
+ FROM information_schema.KEY_COLUMN_USAGE k
227
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS r
228
+ ON r.CONSTRAINT_SCHEMA = k.CONSTRAINT_SCHEMA AND r.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND r.TABLE_NAME = k.TABLE_NAME
229
+ WHERE k.TABLE_SCHEMA = DATABASE() AND k.REFERENCED_TABLE_NAME IS NOT NULL
229
230
  ORDER BY k.TABLE_NAME, k.CONSTRAINT_NAME, k.ORDINAL_POSITION`)) {
230
231
  builder.get(row.table_name).foreignKeys.push({
231
232
  name: row.constraint_name,
@@ -247,7 +248,7 @@ function quoteIdent(name) {
247
248
  return `"${name.replace(/"/g, '""')}"`;
248
249
  }
249
250
  async function createSqliteDriver(url) {
250
- const { DatabaseSync } = await import('node:sqlite');
251
+ const { DatabaseSync } = await loadSqlite();
251
252
  const filePath = sqlitePathFromUrl(url);
252
253
  // readOnly: never create a database file or mutate one while introspecting.
253
254
  const db = filePath === ':memory:' ? new DatabaseSync(':memory:') : new DatabaseSync(filePath, { readOnly: true });
@@ -0,0 +1,3 @@
1
+ /** `node:sqlite` ships with Node.js 22.5+; nativ itself supports Node 20, so SQLite projects get a clear message there. */
2
+ export declare const SQLITE_MIN_NODE = "22.5";
3
+ export declare function loadSqlite(): Promise<typeof import('node:sqlite')>;
@@ -0,0 +1,10 @@
1
+ /** `node:sqlite` ships with Node.js 22.5+; nativ itself supports Node 20, so SQLite projects get a clear message there. */
2
+ export const SQLITE_MIN_NODE = '22.5';
3
+ export async function loadSqlite() {
4
+ try {
5
+ return await import('node:sqlite');
6
+ }
7
+ catch {
8
+ throw new Error(`SQLite databases need Node.js ${SQLITE_MIN_NODE} or newer (this is ${process.version}). Upgrade Node.js, or use Postgres, MySQL or MongoDB.`);
9
+ }
10
+ }
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { withPlanLockSync } from '../core/lock-manager.js';
3
3
  import { recordCircuitBreakerTrip } from '../core/telemetry.js';
4
+ import { notifyHuman } from '../core/notify.js';
4
5
  import { appendEscalation, loadLedger, mutateLedger } from './store.js';
5
6
  const MAX_FAILURE_THRESHOLD = 3;
6
7
  const newEntry = () => ({ consecutiveFailures: 0, lastFailureTime: '', patchCount: 0 });
@@ -38,6 +39,15 @@ function tryMutateLedger(targetDir, mutator) {
38
39
  // Non-fatal
39
40
  }
40
41
  }
42
+ /** Fire-and-forget: the verdict is synchronous, and an open request keeps a CLI process alive until it settles. */
43
+ function notifyTrip(targetDir, taskId, escalationId, ruleId) {
44
+ notifyHuman(targetDir, {
45
+ event: 'circuit_breaker_tripped',
46
+ taskId,
47
+ escalationId,
48
+ summary: `Task ${taskId} failed ${MAX_FAILURE_THRESHOLD} times in a row (${ruleId}) and is now blocked`,
49
+ }).catch(() => { });
50
+ }
41
51
  export class CircuitBreaker {
42
52
  static getStatus(targetDir, taskId) {
43
53
  const ledger = loadLedger(targetDir);
@@ -138,6 +148,7 @@ export class CircuitBreaker {
138
148
  }));
139
149
  this.blockTask(targetDir, taskId, `Circuit breaker tripped by Governor: ${ruleResult.ruleId} (${escalationId ?? 'escalation not recorded'})`);
140
150
  recordCircuitBreakerTrip(targetDir, taskId).catch(() => { });
151
+ notifyTrip(targetDir, taskId, escalationId ?? undefined, ruleResult.ruleId);
141
152
  return { state, escalationId: escalationId ?? undefined, proposal };
142
153
  }
143
154
  /** Atomic increment under the ledger lock: concurrent failures are all counted. */
@@ -192,6 +203,7 @@ export class CircuitBreaker {
192
203
  // 2. Mark task as blocked in .ai/master_plan.json
193
204
  this.blockTask(targetDir, taskId, `Circuit breaker tripped by Governor: ${ruleResult.ruleId} (${escalationId})`);
194
205
  recordCircuitBreakerTrip(targetDir, taskId).catch(() => { });
206
+ notifyTrip(targetDir, taskId, escalationId === 'unrecorded' ? undefined : escalationId, ruleResult.ruleId);
195
207
  return {
196
208
  escalationId,
197
209
  taskId,
@@ -3,6 +3,7 @@ export declare function getContractHash(filePath: string): string | null;
3
3
  export declare function appendAuditLog(targetDir: string, entry: AuditLogEntry): void;
4
4
  export declare function applyDbPatch(schema: any, patch: ContractPatch): any;
5
5
  export declare function applyApiPatch(contracts: any, patch: ContractPatch): any;
6
+ /** False when the write failed; the shared helper retries Windows sharing violations before giving up. */
6
7
  export declare function atomicWriteJson(filePath: string, data: any): boolean;
7
8
  export declare function contractFileFor(target: ContractPatch['target']): string;
8
9
  export declare function applyPatch(contract: any, patch: ContractPatch): any;
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
+ import { writeJsonAtomicSync } from '../core/lock-manager.js';
4
5
  export function getContractHash(filePath) {
5
6
  if (!fs.existsSync(filePath))
6
7
  return null;
@@ -98,18 +99,13 @@ export function applyApiPatch(contracts, patch) {
98
99
  contracts.updatedAt = new Date().toISOString();
99
100
  return contracts;
100
101
  }
102
+ /** False when the write failed; the shared helper retries Windows sharing violations before giving up. */
101
103
  export function atomicWriteJson(filePath, data) {
102
- const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
103
104
  try {
104
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
105
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
106
- fs.renameSync(tmpPath, filePath);
105
+ writeJsonAtomicSync(filePath, data);
107
106
  return true;
108
107
  }
109
108
  catch {
110
- if (fs.existsSync(tmpPath)) {
111
- fs.rmSync(tmpPath, { force: true });
112
- }
113
109
  return false;
114
110
  }
115
111
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { runInit } from './commands/init.js';
3
3
  import { runStatus } from './commands/status.js';
4
4
  import { runValidate } from './commands/validate.js';
5
- import { runTaskList, runTaskNext, runTaskStart, runTaskComplete, runTaskBlock, runTaskEscalate, runTaskAdd, runTaskProposePatch, runTaskUnlock, } from './commands/task.js';
5
+ import { runTaskList, runTaskNext, runTaskStart, runTaskComplete, runTaskBlock, runTaskReclaim, runTaskEscalate, runTaskAdd, runTaskProposePatch, runTaskUnlock, } from './commands/task.js';
6
6
  import { runHookCheck, runHookStatus } from './commands/hook.js';
7
7
  import { runSetup } from './commands/setup.js';
8
8
  import { runDoctor } from './commands/doctor.js';
@@ -15,8 +15,10 @@ import { runVerify } from './commands/verify.js';
15
15
  import { runTestGen, TEST_GEN_FRAMEWORK_HELP } from './commands/test-gen.js';
16
16
  import { runBench } from './commands/bench.js';
17
17
  import { runTriage } from './commands/triage.js';
18
+ import { runNotifyTest } from './commands/notify.js';
18
19
  import { runLearnPropose, runLearnDecide, runLearnList } from './commands/learn.js';
19
20
  import { runProfileCommand } from './commands/profile.js';
21
+ import { runDesignCheck } from './commands/design-check.js';
20
22
  import { packageVersion } from './core/version.js';
21
23
  export function createProgram() {
22
24
  const program = new Command();
@@ -28,6 +30,7 @@ export function createProgram() {
28
30
  .command('init [targetDir]')
29
31
  .description('Scaffold the workflow (.ai/, AGENTS.md, directives) and the Claude Code configuration in the target directory')
30
32
  .option('-f, --force', 'Overwrite existing specification and directive files')
33
+ .option('--design <profile>', 'Design profile: nativ, taste-v1 or taste-v2')
31
34
  .action(async (targetDir, options) => {
32
35
  await runInit(targetDir, options);
33
36
  });
@@ -80,7 +83,8 @@ export function createProgram() {
80
83
  .option('-s, --status <status>', 'Filter tasks by status (pending, in_progress, completed, blocked)')
81
84
  .option('-m, --milestone <id>', 'Filter tasks by milestone ID or name')
82
85
  .option('--fast-path', 'Show only fast-path tasks')
83
- .option('--json', 'Output filtered tasks as JSON')
86
+ .option('--json', 'Output filtered tasks as JSON (a summary per task)')
87
+ .option('--full', 'With --json, include every task field')
84
88
  .action(async (targetDir, options) => {
85
89
  await runTaskList(targetDir, options);
86
90
  });
@@ -121,7 +125,8 @@ export function createProgram() {
121
125
  .option('-s, --status <status>', 'Filter tasks by status (pending, in_progress, completed, blocked)')
122
126
  .option('-m, --milestone <id>', 'Filter tasks by milestone ID or name')
123
127
  .option('--fast-path', 'Show only fast-path tasks')
124
- .option('--json', 'Output filtered tasks as JSON')
128
+ .option('--json', 'Output filtered tasks as JSON (a summary per task)')
129
+ .option('--full', 'With --json, include every task field')
125
130
  .action(async (targetDir, options) => {
126
131
  await runTaskList(targetDir, options);
127
132
  });
@@ -151,9 +156,11 @@ export function createProgram() {
151
156
  });
152
157
  task
153
158
  .command('start <taskId> [targetDir]')
154
- .description('Mark a task as in_progress')
155
- .action(async (taskId, targetDir) => {
156
- await runTaskStart(taskId, targetDir);
159
+ .description('Claim a pending task whose dependencies are done and mark it in_progress')
160
+ .option('--agent <id>', 'Who claims the task (default: $NATIV_AGENT_ID)')
161
+ .option('--force', 'Human override: start a blocked task or one with unfinished dependencies')
162
+ .action(async (taskId, targetDir, options) => {
163
+ await runTaskStart(taskId, targetDir, { agent: options.agent, force: options.force });
157
164
  });
158
165
  task
159
166
  .command('complete <taskId> [targetDir]')
@@ -178,6 +185,16 @@ export function createProgram() {
178
185
  .action(async (taskId, targetDir, options) => {
179
186
  await runVerify(taskId, targetDir, options);
180
187
  });
188
+ task
189
+ .command('reclaim [taskId] [targetDir]')
190
+ .description('Put in_progress tasks whose agent stopped back to pending (claims older than --older-than hours)')
191
+ .option('--older-than <hours>', 'Age at which a claim counts as abandoned (default 4)', parseFloat)
192
+ .option('--dry-run', 'List what would be reclaimed without changing the plan')
193
+ .option('--force', 'Human override: reclaim the named task even though its claim is fresh')
194
+ .option('--json', 'Output the result as JSON')
195
+ .action(async (taskId, targetDir, options) => {
196
+ await runTaskReclaim(taskId, targetDir, { olderThan: options.olderThan, dryRun: options.dryRun, force: options.force, json: options.json });
197
+ });
181
198
  task
182
199
  .command('block <taskId> [targetDir]')
183
200
  .description('Mark a task as blocked with a required reason')
@@ -224,6 +241,15 @@ export function createProgram() {
224
241
  .action(async (taskId, targetDir, options) => {
225
242
  await runTaskUnlock(taskId, targetDir, { reason: options.reason, revoke: options.revoke });
226
243
  });
244
+ const design = program.command('design').description('Design profile tools');
245
+ design
246
+ .command('check [paths...]')
247
+ .description('Scan source files for design-rule violations (default path: src)')
248
+ .option('--profile <id>', 'Design profile: nativ, taste-v1 or taste-v2')
249
+ .option('--json', 'Output findings as JSON')
250
+ .action((paths, options) => {
251
+ runDesignCheck(paths, options);
252
+ });
227
253
  const learn = program
228
254
  .command('learn')
229
255
  .description('Project lessons the contracts do not capture: agents propose, a human approves');
@@ -396,6 +422,14 @@ export function createProgram() {
396
422
  .action(async (escalationId, targetDir, options) => {
397
423
  await runTriage(escalationId, targetDir, options);
398
424
  });
425
+ program
426
+ .command('notify')
427
+ .description('Human notifications (webhook or command) for decisions and circuit-breaker trips')
428
+ .command('test [targetDir]')
429
+ .description('Send a sample notification through the configured webhook or command')
430
+ .action(async (targetDir) => {
431
+ await runNotifyTest(targetDir);
432
+ });
399
433
  program
400
434
  .command('mcp [targetDir]')
401
435
  .description('Run the native MCP (Model Context Protocol) server over stdio, exposing nativ tools and .ai/ contract resources')
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  export { captureOutput } from './capture.js';
3
3
  export { registerMcpResources, MCP_RESOURCES } from './resources.js';
4
- export declare const ESCALATION_TYPES: readonly ["contract_drift", "schema_flaw", "missing_credential", "dependency_conflict", "architectural_ambiguity"];
4
+ export { ESCALATION_TYPES } from '../scanner/types.js';
5
5
  /**
6
6
  * Native MCP (Model Context Protocol) server for Nativ over stdio.
7
7
  *
@@ -1,12 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { Writable } from 'node:stream';
4
5
  import { z } from 'zod';
5
- import { runTaskList, runTaskNext, runTaskStart, runTaskComplete, runTaskBlock, runTaskEscalate, runTaskAdd, runTaskProposePatch, } from '../commands/task.js';
6
- import { SUBAGENT_TYPES } from '../scanner/types.js';
6
+ import { runTaskList, runTaskNext, runTaskStart, runTaskComplete, runTaskBlock, runTaskReclaim, runTaskEscalate, runTaskAdd, runTaskProposePatch, } from '../commands/task.js';
7
+ import { SUBAGENT_TYPES, ESCALATION_TYPES } from '../scanner/types.js';
7
8
  import { runInit } from '../commands/init.js';
9
+ import { runTriage } from '../commands/triage.js';
8
10
  import { runStatus } from '../commands/status.js';
9
11
  import { runDoctor } from '../commands/doctor.js';
12
+ import { runValidate } from '../commands/validate.js';
10
13
  import { runWorktreeCreate, runWorktreeList, runWorktreeMerge, runWorktreeRemove, } from '../commands/worktree.js';
11
14
  import { runDbStatus, runDbInspect, runDbDiff } from '../commands/db.js';
12
15
  import { runVerify } from '../commands/verify.js';
@@ -19,13 +22,7 @@ import { captureOutput } from './capture.js';
19
22
  import { registerMcpResources } from './resources.js';
20
23
  export { captureOutput } from './capture.js';
21
24
  export { registerMcpResources, MCP_RESOURCES } from './resources.js';
22
- export const ESCALATION_TYPES = [
23
- 'contract_drift',
24
- 'schema_flaw',
25
- 'missing_credential',
26
- 'dependency_conflict',
27
- 'architectural_ambiguity',
28
- ];
25
+ export { ESCALATION_TYPES } from '../scanner/types.js';
29
26
  /**
30
27
  * Native MCP (Model Context Protocol) server for Nativ over stdio.
31
28
  *
@@ -46,14 +43,15 @@ export function createMcpServer(targetDirArg) {
46
43
  inputSchema: {},
47
44
  }, () => captureOutput(() => runTaskNext(targetDir, { json: true })));
48
45
  registerNativTool('task_list', {
49
- description: 'List project tasks from .ai/master_plan.json with optional filters (JSON).',
46
+ description: 'List project tasks from .ai/master_plan.json with optional filters (JSON summary; full for every field).',
50
47
  inputSchema: {
51
48
  available: z.boolean().optional().describe('Only tasks that are unblocked and ready for execution'),
52
49
  status: z.enum(['pending', 'in_progress', 'completed', 'blocked']).optional().describe('Filter by task status'),
53
50
  milestone: z.string().optional().describe('Filter by milestone ID or name'),
54
51
  fastPath: z.boolean().optional().describe('Only fast-path tasks'),
52
+ full: z.boolean().optional().describe('Return every task field; the default is a short summary per task'),
55
53
  },
56
- }, ({ available, status, milestone, fastPath }) => captureOutput(() => runTaskList(targetDir, { available, status, milestone, fastPath, json: true })));
54
+ }, ({ available, status, milestone, fastPath, full }) => captureOutput(() => runTaskList(targetDir, { available, status, milestone, fastPath, json: true, brief: !full })));
57
55
  registerNativTool('task_add', {
58
56
  description: 'Append a task to .ai/master_plan.json with an auto-incremented ID (JSON). Goes to the given milestone, else the active one; ' +
59
57
  'fastPath routes it to the fast-path milestone (created on first use).',
@@ -84,9 +82,12 @@ export function createMcpServer(targetDirArg) {
84
82
  json: true,
85
83
  })));
86
84
  registerNativTool('task_start', {
87
- description: 'Mark a task as in_progress.',
88
- inputSchema: { taskId: z.string().min(1).describe('Task ID, e.g. task-12') },
89
- }, ({ taskId }) => captureOutput(() => runTaskStart(taskId, targetDir)));
85
+ description: 'Claim a task and mark it in_progress. Refused when it is blocked, completed, waits on unfinished dependencies, or another agent holds a fresh claim. Calling it again on your own task refreshes the claim.',
86
+ inputSchema: {
87
+ taskId: z.string().min(1).describe('Task ID, e.g. task-12'),
88
+ agent: z.string().optional().describe('Your agent id, so parallel agents do not take the same task'),
89
+ },
90
+ }, ({ taskId, agent }) => captureOutput(() => runTaskStart(taskId, targetDir, { agent })));
90
91
  registerNativTool('task_complete', {
91
92
  description: "Mark a task as completed (always executes its verificationCommand; verification cannot be skipped over MCP). Advances the milestone when all its tasks are done.",
92
93
  inputSchema: {
@@ -115,6 +116,33 @@ export function createMcpServer(targetDirArg) {
115
116
  reason: z.string().min(1).describe('Why the task is blocked'),
116
117
  },
117
118
  }, ({ taskId, reason }) => captureOutput(() => runTaskBlock(taskId, reason, targetDir)));
119
+ registerNativTool('triage', {
120
+ description: 'Self-heal escalations: evaluate pending escalations (or one), apply sandbox-proven additive contract fixes and unblock their tasks. Returns compact JSON; items classified REQUIRE_HUMAN_DECISION carry a humanCard to present to the human.',
121
+ inputSchema: {
122
+ escalationId: z.string().optional().describe('Triage only this escalation, e.g. esc-03; default: every pending one'),
123
+ apply: z.boolean().optional().describe('Write proven safe patches and unblock tasks (default true)'),
124
+ },
125
+ }, ({ escalationId, apply }) => captureOutput(() => runTriage(escalationId, targetDir, {
126
+ all: !escalationId,
127
+ apply: apply ?? true,
128
+ json: true,
129
+ interactive: false,
130
+ // stdout carries the MCP protocol, so triage output is routed into the captured tool result.
131
+ output: new Writable({
132
+ write(chunk, _encoding, done) {
133
+ console.log(String(chunk).replace(/\n$/, ''));
134
+ done();
135
+ },
136
+ }),
137
+ })));
138
+ registerNativTool('task_reclaim', {
139
+ description: 'Put in_progress tasks whose agent stopped (claim older than olderThanHours, default 4) back to pending. Pass taskId to reclaim one task that has a stale or unrecorded claim.',
140
+ inputSchema: {
141
+ taskId: z.string().optional().describe('Reclaim only this task'),
142
+ olderThanHours: z.number().positive().optional().describe('Age at which a claim counts as abandoned'),
143
+ dryRun: z.boolean().optional().describe('Report without changing the plan'),
144
+ },
145
+ }, ({ taskId, olderThanHours, dryRun }) => captureOutput(() => runTaskReclaim(taskId, targetDir, { olderThan: olderThanHours, dryRun, json: true })));
118
146
  registerNativTool('task_escalate', {
119
147
  description: 'Escalate a contract or architectural blocker to the Architect (writes .ai/escalation.json and blocks the task).',
120
148
  inputSchema: {
@@ -192,6 +220,17 @@ export function createMcpServer(targetDirArg) {
192
220
  description: 'Summarize execution progress across all milestones and tasks.',
193
221
  inputSchema: {},
194
222
  }, () => captureOutput(() => runStatus(targetDir)));
223
+ registerNativTool('validate', {
224
+ description: 'Read-only integrity check of the .ai/ contracts, master plan, spec references and role guides. ' +
225
+ 'Returns only problems (✖ errors, ⚠ warnings) and the verdict; an error result means a contract is missing or malformed.',
226
+ inputSchema: {},
227
+ }, async () => {
228
+ const result = await captureOutput(() => runValidate(targetDir));
229
+ const text = result.content[0]?.type === 'text' ? result.content[0].text : '';
230
+ const problems = text.split('\n').filter((line) => /[✖⚠]/.test(line) && !/validation failed with errors/i.test(line)).map((line) => line.trim());
231
+ const verdict = result.isError ? 'Validation failed.' : 'All contracts are valid.';
232
+ return { ...result, content: [{ type: 'text', text: [...problems, verdict].join('\n') }] };
233
+ });
195
234
  registerNativTool('doctor', {
196
235
  description: 'Read-only health check of the Claude Code integration: MCP server, hooks, permissions, agents, enforcement mode and provider availability (JSON). ' +
197
236
  'Reports drift but never repairs it; repairing is `nativ doctor --fix` in a terminal.',
@@ -68,6 +68,9 @@ export function createClaudeApiProvider(deps = {}) {
68
68
  throw new ProviderError('NO_CREDENTIALS', 'The Anthropic API key was rejected.');
69
69
  if (/timeout|timed out/i.test(name + message))
70
70
  throw new ProviderError('TIMEOUT', `Claude did not answer within ${timeout}ms.`);
71
+ if ((status !== undefined && status >= 500) || /APIConnectionError|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|socket hang up|fetch failed/i.test(name + message)) {
72
+ throw new ProviderError('TRANSIENT', message.slice(0, 300));
73
+ }
71
74
  throw new ProviderError('BAD_RESPONSE', message.slice(0, 300));
72
75
  }
73
76
  },
@@ -52,7 +52,8 @@ export function createGeminiProvider(deps = {}) {
52
52
  const name = err?.name ?? '';
53
53
  if (name === 'TimeoutError' || name === 'AbortError')
54
54
  throw new ProviderError('TIMEOUT', `Gemini did not answer within ${timeout}ms.`);
55
- throw new ProviderError('BAD_RESPONSE', err instanceof Error ? err.message : String(err));
55
+ // fetch only throws here when the request never got an answer: DNS, refused or reset connection.
56
+ throw new ProviderError('TRANSIENT', err instanceof Error ? err.message : String(err));
56
57
  }
57
58
  if (res.status === 429) {
58
59
  const seconds = Number(res.headers.get('retry-after'));
@@ -60,6 +61,8 @@ export function createGeminiProvider(deps = {}) {
60
61
  }
61
62
  if (res.status === 401 || res.status === 403)
62
63
  throw new ProviderError('NO_CREDENTIALS', 'The Gemini API key was rejected.');
64
+ if (res.status >= 500)
65
+ throw new ProviderError('TRANSIENT', `Gemini responded with HTTP ${res.status}.`);
63
66
  if (!res.ok)
64
67
  throw new ProviderError('BAD_RESPONSE', `Gemini responded with HTTP ${res.status}.`);
65
68
  let body;
@@ -31,8 +31,12 @@ export declare function providerCooldownRemaining(root: string, id: ProviderId,
31
31
  export declare function resolveProviderChain(role: ProviderRole, root: string, options?: RegistryOptions): Provider[];
32
32
  export declare function resolveProvider(role: ProviderRole, root: string, options?: RegistryOptions): Provider | null;
33
33
  /**
34
- * Runs the request against each provider in turn. A rate limit puts that provider on cooldown, and any
35
- * ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
34
+ * Runs the request against each provider in turn. A TRANSIENT failure (network, 5xx) is retried on the same
35
+ * provider up to MAX_TRANSIENT_RETRIES times with jittered backoff. A rate limit puts that provider on cooldown, and
36
+ * any other ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
36
37
  * last error when every provider failed, so the caller can drop to its deterministic path.
37
38
  */
39
+ export declare const MAX_TRANSIENT_RETRIES = 2;
40
+ /** 0.5s, then 1s, each plus up to 50% jitter so parallel agents do not retry in lockstep. */
41
+ export declare function transientBackoffMs(attempt: number, random?: () => number): number;
38
42
  export declare function completeWithChain(role: ProviderRole, root: string, request: CompleteRequest, options?: RegistryOptions): Promise<ChainResult>;