atris 3.45.1 → 3.46.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.
@@ -5,7 +5,6 @@ const { spawnSync } = require('child_process');
5
5
  const { hasFlag } = require('../lib/arg-parser');
6
6
 
7
7
  const SCHEMA = 'atris.codex_goal.v1';
8
- const CONFIRM_RESET_FLAG = '--confirm-complete-goal-reset';
9
8
 
10
9
  // Preserve the existing rule that the next token is a value, even if it is a flag.
11
10
  function readFollowingFlag(args, name, fallback = '') {
@@ -21,10 +20,6 @@ function expandHome(filePath) {
21
20
  return filePath;
22
21
  }
23
22
 
24
- function safeStamp(value = new Date().toISOString()) {
25
- return value.replace(/[:.]/g, '-');
26
- }
27
-
28
23
  function sqlString(value) {
29
24
  return `'${String(value).replace(/'/g, "''")}'`;
30
25
  }
@@ -76,27 +71,6 @@ function runGoalQuery(args, buildSql) {
76
71
  return runSqliteJson(ctx.goalsDb, ctx.prefix + buildSql(ctx.threadsTable));
77
72
  }
78
73
 
79
- function defaultRunsDir(args = []) {
80
- return path.resolve(readFollowingFlag(args, '--out-dir', path.join(process.cwd(), '.atris', 'runs')));
81
- }
82
-
83
- function ensurePrivateDir(dir) {
84
- fs.mkdirSync(dir, { recursive: true });
85
- try {
86
- fs.chmodSync(dir, 0o700);
87
- } catch {
88
- // Best effort: some filesystems do not support POSIX permissions.
89
- }
90
- }
91
-
92
- function chmodPrivate(filePath) {
93
- try {
94
- fs.chmodSync(filePath, 0o600);
95
- } catch {
96
- // Best effort: some filesystems do not support POSIX permissions.
97
- }
98
- }
99
-
100
74
  function runSqliteOnce(dbPath, sql, readonly) {
101
75
  const sqliteArgs = [];
102
76
  if (readonly) sqliteArgs.push('-readonly');
@@ -202,25 +176,6 @@ function resolveThreadGoal(args) {
202
176
  return null;
203
177
  }
204
178
 
205
- function writeReceipt(outDir, payload) {
206
- ensurePrivateDir(outDir);
207
- const receiptPath = path.join(outDir, `codex-goal-${payload.action}-${safeStamp(payload.finished_at || payload.started_at)}.json`);
208
- const withPath = { ...payload, receipt_path: receiptPath };
209
- fs.writeFileSync(receiptPath, `${JSON.stringify(withPath, null, 2)}\n`, 'utf8');
210
- chmodPrivate(receiptPath);
211
- return withPath;
212
- }
213
-
214
- function backupSqliteDb(dbPath, backupPath) {
215
- const result = spawnSync('sqlite3', [dbPath, `VACUUM INTO ${sqlString(backupPath)};`], { encoding: 'utf8' });
216
- if (result.error) throw new Error(`sqlite3 backup failed: ${result.error.message}`);
217
- if (result.status !== 0) {
218
- const detail = (result.stderr || result.stdout || '').trim();
219
- throw new Error(detail || `sqlite3 backup exited with status ${result.status}`);
220
- }
221
- chmodPrivate(backupPath);
222
- }
223
-
224
179
  function printJsonOrText(payload, lines, asJson) {
225
180
  if (asJson) {
226
181
  console.log(JSON.stringify(payload, null, 2));
@@ -252,92 +207,41 @@ function statusCommand(args) {
252
207
  printJsonOrText(payload, [
253
208
  `Codex goals: ${goals.length} recent`,
254
209
  ...goals.map((row) => `- ${row.status} ${row.thread_id}: ${row.objective}`),
255
- 'Reset requires --thread <id> or --latest.',
210
+ 'Completed tasks stay closed. Start new work in a new Codex task.',
256
211
  ], asJson);
257
212
  }
258
213
 
259
214
  function resetCommand(args) {
260
215
  const asJson = hasFlag(args, '--json');
261
216
  const dbPath = resolveStatePath(args);
262
- const outDir = defaultRunsDir(args);
263
217
  ensureStateDb(dbPath);
264
218
 
265
- const startedAt = new Date().toISOString();
266
219
  const goal = resolveThreadGoal(args);
267
220
  if (!goal) {
268
221
  throw new Error('No Codex goal found. Pass --thread <thread-id> or --latest.');
269
222
  }
270
- if (goal.status !== 'complete') {
271
- throw new Error(`Refusing to reset ${goal.status} goal. Complete the native Codex goal first.`);
272
- }
273
- if (!hasFlag(args, CONFIRM_RESET_FLAG)) {
274
- const payload = {
275
- ok: false,
276
- schema: SCHEMA,
277
- action: 'reset',
278
- status: 'needs_confirmation',
279
- state_path: dbPath,
280
- goal,
281
- required_flag: CONFIRM_RESET_FLAG,
282
- finished_at: new Date().toISOString(),
283
- };
284
- printJsonOrText(payload, [
285
- 'Codex goal reset blocked: confirmation required.',
286
- `Thread: ${goal.thread_id}`,
287
- `Objective: ${goal.objective}`,
288
- `Run again with ${CONFIRM_RESET_FLAG} to back up state and clear this completed goal slot.`,
289
- ], asJson);
290
- process.exitCode = 1;
291
- return;
292
- }
293
-
294
- ensurePrivateDir(outDir);
295
- const stamp = safeStamp(startedAt);
296
- const backupPath = path.join(outDir, `codex-state-before-goal-reset-${goal.thread_id}-${stamp}.sqlite`);
297
- const dumpPath = path.join(outDir, `codex-goal-row-before-reset-${goal.thread_id}-${stamp}.json`);
298
- backupSqliteDb(dbPath, backupPath);
299
- fs.writeFileSync(dumpPath, `${JSON.stringify(goal, null, 2)}\n`, 'utf8');
300
- chmodPrivate(dumpPath);
301
-
302
- const rows = runSqliteJson(dbPath, `
303
- BEGIN IMMEDIATE;
304
- DELETE FROM thread_goals
305
- WHERE thread_id = ${sqlString(goal.thread_id)}
306
- AND goal_id = ${sqlString(goal.goal_id)}
307
- AND status = 'complete';
308
- SELECT changes() AS deleted;
309
- COMMIT;
310
- `, { readonly: false });
311
- const deleted = Number(rows[0]?.deleted || 0);
312
- const remaining = readGoalByThread(args, goal.thread_id);
313
- const ok = deleted === 1 && !remaining;
314
- const payload = writeReceipt(outDir, {
315
- ok,
223
+ const completed = goal.status === 'complete';
224
+ const nextAction = completed
225
+ ? 'Create a new Codex task for new or recurring work. Leave this completed task closed.'
226
+ : 'Continue or hand off the current task without clearing its goal.';
227
+ const payload = {
228
+ ok: false,
316
229
  schema: SCHEMA,
317
230
  action: 'reset',
318
- status: ok ? 'reset' : 'failed',
319
- thread_id: goal.thread_id,
320
- goal_id: goal.goal_id,
321
- objective: goal.objective,
322
- previous_status: goal.status,
323
- deleted,
231
+ status: completed ? 'completed_task_closed' : 'active_task_unchanged',
324
232
  state_path: dbPath,
325
- backup_path: backupPath,
326
- dump_path: dumpPath,
327
- started_at: startedAt,
233
+ goal,
234
+ mutated: false,
328
235
  finished_at: new Date().toISOString(),
329
- next_action: ok ? 'Call the native Codex create_goal tool in this same thread, then keep Atris Mission/member state as the durable loop.' : 'Inspect backup/dump before retrying.',
330
- });
236
+ next_action: nextAction,
237
+ };
331
238
 
332
239
  printJsonOrText(payload, [
333
- ok ? 'Codex goal slot reset.' : 'Codex goal reset failed.',
334
- `Thread: ${goal.thread_id}`,
335
- `Backup: ${path.relative(process.cwd(), backupPath)}`,
336
- `Dump: ${path.relative(process.cwd(), dumpPath)}`,
337
- `Receipt: ${path.relative(process.cwd(), payload.receipt_path)}`,
338
- `Next: ${payload.next_action}`,
240
+ completed ? 'Completed Codex task stays closed.' : `Codex goal reset refused: this task is ${goal.status}.`,
241
+ `Objective: ${goal.objective}`,
242
+ `Next: ${nextAction}`,
339
243
  ], asJson);
340
- if (!ok) process.exitCode = 1;
244
+ process.exitCode = 1;
341
245
  }
342
246
 
343
247
  function usage() {
@@ -345,19 +249,17 @@ function usage() {
345
249
  'atris codex-goal - guarded bridge for native Codex thread goals',
346
250
  '',
347
251
  ' atris codex-goal status [--thread <id>|--latest] [--json]',
348
- ` atris codex-goal reset --thread <id> ${CONFIRM_RESET_FLAG}`,
252
+ ' atris codex-goal reset --thread <id> [--json] Report why the task cannot be reset',
349
253
  '',
350
254
  'Flags:',
351
255
  ' --state <path> Codex goals DB (default ~/.codex/goals_1.sqlite, falls back to state_5.sqlite)',
352
256
  ' --threads-db <path> Codex thread metadata DB for cwd/title (default ~/.codex/state_5.sqlite)',
353
257
  ' --latest Use the latest Codex goal whose thread cwd matches the current directory',
354
- ' --out-dir <path> Receipt/backup directory (default .atris/runs)',
355
258
  '',
356
- 'Reset guardrails:',
357
- '- only completed native Codex goals can be reset',
358
- '- reset backs up the SQLite DB before mutation',
359
- '- reset dumps the exact deleted row and writes a receipt',
360
- '- the next native goal must still be created by the active Codex thread',
259
+ 'Task boundary:',
260
+ '- active tasks continue in their current thread',
261
+ '- completed tasks retain their final goal state',
262
+ '- new work and recurring monitors use a new dedicated Codex task',
361
263
  ].join('\n');
362
264
  }
363
265
 
package/commands/init.js CHANGED
@@ -3,6 +3,12 @@ const path = require('path');
3
3
  const { ensureExperimentsFramework } = require('./experiments');
4
4
  const { ensureWikiScaffold } = require('../lib/wiki');
5
5
  const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
6
+ const {
7
+ upsertAgentVoiceCard,
8
+ upsertClaudeVoiceHook,
9
+ upsertCursorVoiceCard,
10
+ voiceCardForRoot,
11
+ } = require('../lib/voice-card');
6
12
 
7
13
  /**
8
14
  * Detect project context by scanning project structure
@@ -732,6 +738,12 @@ Every agent should leave four artifacts another agent can trust:
732
738
  | Proof ready | \`atris task ready <id> --proof "<commands or receipt>" --result "<day-one PM sentence>"\` |
733
739
  | Human accept | \`atris task accept <id>\` |
734
740
 
741
+ Every created task leads with three plain fields: what changes, why it matters,
742
+ and what done looks like. Keep the exact title, files, commands, constraints,
743
+ events, and proof underneath unchanged. Planned work offers approve or ask for
744
+ a change through the existing Plan/Do gates; finished work uses the existing
745
+ accept/revise gates and never skips proof.
746
+
735
747
  Do not rely on chat context. Put the task, file pointers, and proof on disk.
736
748
  Do not write new operating doctrine here first; add it to Atris policy, skills,
737
749
  wiki, or \`atris/atris.md\`, then regenerate this adapter if needed.
@@ -758,7 +770,8 @@ Human accept -> task Done + AgentXP awarded
758
770
  \`\`\`
759
771
 
760
772
  Always-on agents should move proof-backed work to Review, complete their native
761
- goal, then continue the mission loop with the next goal. They must not run
773
+ goal, then stop that task. The next goal or recurring monitor starts in a new
774
+ dedicated task. They must not run
762
775
  \`atris task accept\` or claim AgentXP unless a human approved the proof.
763
776
 
764
777
  Mission-shaped user intent wins before normal task selection. If the user
@@ -831,6 +844,8 @@ member -> mission start --verify -> status --status active -> one bounded step -
831
844
 
832
845
  **Protocol:** See \`atris/atris.md\` for full spec.`;
833
846
 
847
+ const voiceCard = voiceCardForRoot(process.cwd());
848
+
834
849
  // .cursorrules for Cursor (legacy)
835
850
  const cursorRulesFile = path.join(process.cwd(), '.cursorrules');
836
851
  if (!fs.existsSync(cursorRulesFile)) {
@@ -847,12 +862,22 @@ member -> mission start --verify -> status --status active -> one bounded step -
847
862
  markReady('adapters', '.cursor/rules/atris.mdc', '✓ Created .cursor/rules/atris.mdc (for Cursor)');
848
863
  }
849
864
 
865
+ const cursorVoiceFile = path.join(cursorRulesDir, 'atris-voice.mdc');
866
+ const cursorVoiceResult = upsertCursorVoiceCard(cursorVoiceFile, voiceCard);
867
+ if (cursorVoiceResult.action !== 'unchanged') {
868
+ markReady('adapters', '.cursor/rules/atris-voice.mdc', '✓ Pinned the Atris voice card for Cursor');
869
+ }
870
+
850
871
  // AGENTS.md for Codex
851
872
  const agentsMdFile = path.join(process.cwd(), 'AGENTS.md');
852
873
  if (!fs.existsSync(agentsMdFile)) {
853
874
  fs.writeFileSync(agentsMdFile, agentInstructions);
854
875
  markReady('adapters', 'AGENTS.md', '✓ Created AGENTS.md (for Codex)');
855
876
  }
877
+ const agentsVoiceResult = upsertAgentVoiceCard(agentsMdFile, voiceCard);
878
+ if (agentsVoiceResult.action !== 'unchanged') {
879
+ markReady('adapters', 'AGENTS.md voice card', '✓ Pinned the Atris voice card in AGENTS.md');
880
+ }
856
881
 
857
882
  // .devin/config.local.json for Devin for Terminal
858
883
  const devinConfigDir = path.join(process.cwd(), '.devin');
@@ -1035,14 +1060,11 @@ Read atris/MAP.md. Begin iteration 1.`;
1035
1060
  markReady('adapters', 'atris/CLAUDE.md', '✓ Created atris/CLAUDE.md (for Claude Code)');
1036
1061
  }
1037
1062
 
1038
- // .claude/settings.json with SessionStart hook for auto-loading Atris
1063
+ // .claude/settings.json with startup and per-prompt Atris hooks
1039
1064
  const claudeSettingsDir = path.join(process.cwd(), '.claude');
1040
1065
  const claudeSettingsFile = path.join(claudeSettingsDir, 'settings.json');
1041
- if (!fs.existsSync(claudeSettingsFile)) {
1042
- if (!fs.existsSync(claudeSettingsDir)) {
1043
- fs.mkdirSync(claudeSettingsDir, { recursive: true });
1044
- }
1045
- const claudeSettings = {
1066
+ const claudeSettingsResult = upsertClaudeVoiceHook(claudeSettingsFile, {
1067
+ initialSettings: {
1046
1068
  hooks: {
1047
1069
  SessionStart: [
1048
1070
  {
@@ -1065,9 +1087,10 @@ Read atris/MAP.md. Begin iteration 1.`;
1065
1087
  }
1066
1088
  ]
1067
1089
  }
1068
- };
1069
- fs.writeFileSync(claudeSettingsFile, JSON.stringify(claudeSettings, null, 2));
1070
- markReady('adapters', '.claude/settings.json', '✓ Created .claude/settings.json (auto-loads Atris on startup)');
1090
+ },
1091
+ });
1092
+ if (claudeSettingsResult.action !== 'unchanged' && claudeSettingsResult.action !== 'skipped') {
1093
+ markReady('adapters', '.claude/settings.json', '✓ Wired Atris into Claude startup and replies');
1071
1094
  }
1072
1095
 
1073
1096
  // Co-author trailer: commits in this workspace credit Atris, same as Claude/Cursor do