@ouro.bot/cli 0.1.0-alpha.655 → 0.1.0-alpha.658

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 (55) hide show
  1. package/README.md +13 -13
  2. package/changelog.json +21 -0
  3. package/dist/arc/evolution.js +1 -1
  4. package/dist/arc/flight-recorder.js +369 -0
  5. package/dist/arc/obligations.js +24 -2
  6. package/dist/heart/active-work.js +1 -1
  7. package/dist/heart/config-registry.js +14 -5
  8. package/dist/heart/daemon/agent-config-check.js +1 -1
  9. package/dist/heart/daemon/agent-service.js +18 -17
  10. package/dist/heart/daemon/cli-exec.js +134 -15
  11. package/dist/heart/daemon/cli-help.js +21 -2
  12. package/dist/heart/daemon/cli-parse.js +31 -3
  13. package/dist/heart/daemon/daemon-entry.js +1 -1
  14. package/dist/heart/daemon/daemon.js +3 -3
  15. package/dist/heart/daemon/hooks/bundle-meta.js +29 -9
  16. package/dist/heart/daemon/inner-status.js +4 -15
  17. package/dist/heart/daemon/sense-manager.js +16 -1
  18. package/dist/heart/habits/habit-parser.js +64 -1
  19. package/dist/heart/hatch/hatch-flow.js +17 -9
  20. package/dist/heart/hatch/specialist-tools.js +15 -11
  21. package/dist/heart/identity.js +4 -1
  22. package/dist/heart/kept-notes.js +5 -73
  23. package/dist/heart/mailbox/readers/runtime-readers.js +21 -49
  24. package/dist/heart/mcp/mcp-server.js +8 -8
  25. package/dist/heart/sense-truth.js +2 -0
  26. package/dist/heart/session-events.js +1 -31
  27. package/dist/heart/start-of-turn-packet.js +8 -2
  28. package/dist/heart/tool-description.js +15 -3
  29. package/dist/heart/turn-context.js +34 -7
  30. package/dist/heart/work-card.js +386 -0
  31. package/dist/mailbox-ui/assets/{index-9-AxCxuB.js → index-Cbasiy6y.js} +1 -1
  32. package/dist/mailbox-ui/index.html +1 -1
  33. package/dist/mind/bundle-manifest.js +9 -3
  34. package/dist/mind/context.js +1 -2
  35. package/dist/mind/desk-section.js +53 -1
  36. package/dist/mind/diary.js +2 -3
  37. package/dist/mind/note-search.js +36 -106
  38. package/dist/mind/prompt.js +45 -102
  39. package/dist/mind/record-paths.js +312 -0
  40. package/dist/repertoire/bundle-templates.js +4 -5
  41. package/dist/repertoire/tools-bundle.js +1 -1
  42. package/dist/repertoire/tools-evolution.js +4 -4
  43. package/dist/repertoire/tools-notes.js +42 -62
  44. package/dist/repertoire/tools-record.js +16 -11
  45. package/dist/repertoire/tools-session.js +4 -4
  46. package/dist/repertoire/tools.js +1 -1
  47. package/dist/senses/habit-turn-message.js +19 -5
  48. package/dist/senses/inner-dialog-worker.js +58 -9
  49. package/dist/senses/inner-dialog.js +30 -11
  50. package/dist/senses/pipeline.js +135 -1
  51. package/dist/util/frontmatter.js +17 -1
  52. package/package.json +3 -3
  53. package/skills/configure-dev-tools.md +1 -1
  54. package/skills/travel-planning.md +1 -1
  55. package/dist/mind/journal-index.js +0 -162
@@ -14,7 +14,7 @@
14
14
  * - Build artifacts (rare in bundles, but possible).
15
15
  *
16
16
  * It DOES NOT handle PII. The bundle is inherently full of PII — `friends/`,
17
- * `diary/`, `journal/`, `psyche/`, `arc/`, `facts/`, `family/`, `travel/`
17
+ * `desk/_record/`, `psyche/`, `arc/`, `facts/`, `family/`, `travel/`
18
18
  * etc. That's the point of the bundle; blocking those via .gitignore would
19
19
  * defeat the purpose.
20
20
  *
@@ -54,19 +54,18 @@ node_modules/
54
54
  dist/
55
55
  `;
56
56
  /**
57
- * PII-sensitive top-level directories. Enumerated here so `bundle_first_push_review`
57
+ * PII-sensitive bundle directories. Enumerated here so `bundle_first_push_review`
58
58
  * can categorize and count. Adding a new PII bucket to the bundle means adding
59
59
  * it here so the first-push warning includes it.
60
60
  */
61
61
  exports.PII_BUNDLE_DIRECTORIES = [
62
62
  "friends",
63
- "diary",
64
- "journal",
63
+ "desk/_record/diary",
64
+ "desk/_record/notes",
65
65
  "psyche",
66
66
  "arc",
67
67
  "facts",
68
68
  "family",
69
69
  "travel",
70
- "notes",
71
70
  "sessions",
72
71
  ];
@@ -944,7 +944,7 @@ exports.bundleToolDefinitions = [
944
944
  type: "function",
945
945
  function: {
946
946
  name: "bundle_first_push_review",
947
- description: "Review my bundle for PII exposure before the first push to a new remote. Enumerates PII-bearing directories (friends, diary, journal, etc.) with per-directory counts, probes the remote URL for GitHub public/private visibility, and returns a first-person warning text I must show the human plus a confirmationToken I must pass to bundle_push on first push. Required before the first push to any new remote.",
947
+ description: "Review my bundle for PII exposure before the first push to a new remote. Enumerates PII-bearing directories (friends, Desk record, Arc, etc.) with per-directory counts, probes the remote URL for GitHub public/private visibility, and returns a first-person warning text I must show the human plus a confirmationToken I must pass to bundle_push on first push. Required before the first push to any new remote.",
948
948
  parameters: { type: "object", properties: {} },
949
949
  },
950
950
  },
@@ -8,7 +8,7 @@ const runtime_1 = require("../nerves/runtime");
8
8
  const EVOLUTION_ACTIONS = new Set([
9
9
  "create_case",
10
10
  "add_evidence",
11
- "write_journal",
11
+ "write_record",
12
12
  "write_desk",
13
13
  "write_diary",
14
14
  "spawn_coding",
@@ -46,7 +46,7 @@ const EVIDENCE_KINDS = new Set([
46
46
  "release",
47
47
  "installed_runtime",
48
48
  "diary_entry",
49
- "journal_file",
49
+ "desk_record_note",
50
50
  "skill_file",
51
51
  "sense_artifact",
52
52
  "hosted_audit",
@@ -54,7 +54,7 @@ const EVIDENCE_KINDS = new Set([
54
54
  "external_doc",
55
55
  ]);
56
56
  const REDACTIONS = new Set(["none", "summary", "private_ref", "secret_ref"]);
57
- const DECISIONS = new Set(["ignore", "defer", "journal", "ask", "delegate", "act", "abandon"]);
57
+ const DECISIONS = new Set(["ignore", "defer", "record", "ask", "delegate", "act", "abandon"]);
58
58
  const VERIFICATION_STATUSES = new Set(["not-verified", "partial", "passed", "failed"]);
59
59
  const RATIFICATION_DESTINATIONS = new Set([
60
60
  "code",
@@ -63,7 +63,7 @@ const RATIFICATION_DESTINATIONS = new Set([
63
63
  "desk_lesson",
64
64
  "desk_task",
65
65
  "diary",
66
- "journal",
66
+ "desk_record",
67
67
  "habit",
68
68
  "policy",
69
69
  "agent_config",
@@ -1,45 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.notesToolDefinitions = void 0;
37
- const fs = __importStar(require("fs"));
38
- const path = __importStar(require("path"));
39
4
  const child_process_1 = require("child_process");
40
5
  const skills_1 = require("./skills");
41
6
  const config_1 = require("../heart/config");
42
- const identity_1 = require("../heart/identity");
43
7
  const runtime_1 = require("../nerves/runtime");
44
8
  const diary_1 = require("../mind/diary");
45
9
  const provenance_trust_1 = require("../mind/provenance-trust");
@@ -170,8 +134,8 @@ exports.notesToolDefinitions = [
170
134
  tool: {
171
135
  type: "function",
172
136
  function: {
173
- name: "search_notes",
174
- description: "Search my diary and journal for facts, thoughts, and working notes matching a query. Uses semantic similarity -- phrasing matters. Try different angles if the first query doesn't find what you're looking for. Search written notes before asking the human something the notes may already answer.",
137
+ name: "search_facts",
138
+ description: "Search my Desk record diary facts matching a query. Uses semantic similarity -- phrasing matters. Try different angles if the first query doesn't find what you're looking for. Search written facts before asking the human something the record may already answer.",
175
139
  parameters: {
176
140
  type: "object",
177
141
  properties: { query: { type: "string" } },
@@ -185,7 +149,6 @@ exports.notesToolDefinitions = [
185
149
  if (!query)
186
150
  return "query is required";
187
151
  const resultLines = [];
188
- // Search diary entries
189
152
  const hits = await (0, diary_1.searchDiaryEntries)(query, (0, diary_1.readDiaryEntries)());
190
153
  for (const fact of hits) {
191
154
  let meta = `source=${fact.source}, createdAt=${fact.createdAt}`;
@@ -200,35 +163,52 @@ exports.notesToolDefinitions = [
200
163
  const tag = (0, provenance_trust_1.classifyProvenanceTrust)(fact.provenance) === "external" ? "diary/external" : "diary";
201
164
  resultLines.push(`[${tag}] ${fact.text} (${meta})`);
202
165
  }
203
- // Search journal index
204
- const agentRoot = (0, identity_1.getAgentRoot)();
205
- const journalIndexPath = path.join(agentRoot, "journal", ".index.json");
206
- try {
207
- const raw = fs.readFileSync(journalIndexPath, "utf8");
208
- const journalEntries = JSON.parse(raw);
209
- if (Array.isArray(journalEntries) && journalEntries.length > 0) {
210
- // Substring match on preview and filename
211
- const lowerQuery = query.toLowerCase();
212
- for (const entry of journalEntries) {
213
- /* v8 ignore next 4 -- both sides tested (filename-only match in search_notes-journal.test.ts); v8 misreports || short-circuit @preserve */
214
- if (entry.preview.toLowerCase().includes(lowerQuery) ||
215
- entry.filename.toLowerCase().includes(lowerQuery)) {
216
- resultLines.push(`[journal] ${entry.filename}: ${entry.preview}`);
217
- }
218
- }
219
- }
220
- }
221
- catch {
222
- // No journal index or malformed — skip journal search
223
- }
224
166
  return resultLines.join("\n");
225
167
  }
226
168
  catch (e) {
227
- return `error: ${e instanceof Error ? e.message : String(e)}`;
169
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
228
170
  }
229
171
  },
230
172
  summaryKeys: ["query"],
231
173
  },
174
+ {
175
+ tool: {
176
+ type: "function",
177
+ function: {
178
+ name: "consult_diary",
179
+ description: "Inspect my Desk record diary facts. With a query, searches semantically. Without a query, returns the most recent facts. Use this for direct record inspection before asking someone to restate something already written down.",
180
+ parameters: {
181
+ type: "object",
182
+ properties: {
183
+ query: { type: "string" },
184
+ limit: { type: "string" },
185
+ },
186
+ },
187
+ },
188
+ },
189
+ handler: async (a) => {
190
+ try {
191
+ const limitRaw = a.limit ? Number.parseInt(String(a.limit), 10) : 10;
192
+ const limit = Number.isFinite(limitRaw) ? Math.min(50, Math.max(1, limitRaw)) : 10;
193
+ const query = typeof a.query === "string" ? a.query.trim() : "";
194
+ const facts = (0, diary_1.readDiaryEntries)();
195
+ const matches = query ? await (0, diary_1.searchDiaryEntries)(query, facts) : [...facts].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
196
+ const items = matches.slice(0, limit).map((fact) => ({
197
+ id: fact.id,
198
+ text: fact.text,
199
+ source: fact.source,
200
+ createdAt: fact.createdAt,
201
+ ...(fact.about ? { about: fact.about } : {}),
202
+ ...(fact.provenance ? { provenance: fact.provenance } : {}),
203
+ }));
204
+ return JSON.stringify({ items });
205
+ }
206
+ catch (e) {
207
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
208
+ }
209
+ },
210
+ summaryKeys: ["query", "limit"],
211
+ },
232
212
  {
233
213
  tool: {
234
214
  type: "function",
@@ -275,7 +255,7 @@ exports.notesToolDefinitions = [
275
255
  return `saved diary entry (added=${result.added}, skipped=${result.skipped})`;
276
256
  },
277
257
  summaryKeys: ["entry", "about"],
278
- riskProfile: { mutates: "durable_state_write", risk: "high", reason: "writes diary memory" },
258
+ riskProfile: { mutates: "durable_state_write", risk: "high", reason: "writes Desk record diary fact" },
279
259
  },
280
260
  {
281
261
  tool: {
@@ -37,9 +37,10 @@ exports.recordToolDefinitions = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const session_events_1 = require("../heart/session-events");
40
- const identity_1 = require("../heart/identity");
41
40
  const embedding_provider_1 = require("../mind/embedding-provider");
42
41
  const note_search_1 = require("../mind/note-search");
42
+ const record_paths_1 = require("../mind/record-paths");
43
+ const types_1 = require("../mind/friends/types");
43
44
  const runtime_1 = require("../nerves/runtime");
44
45
  const NOTES_INDEX_VERSION = 1;
45
46
  const NOTE_SLUG_MAX_CHARS = 40;
@@ -54,6 +55,12 @@ function hasSelfTrust(ctx) {
54
55
  const friend = ctx?.context?.friend;
55
56
  return !friend || friend.id === "self";
56
57
  }
58
+ function hasRecordReadTrust(ctx) {
59
+ if (hasSelfTrust(ctx))
60
+ return true;
61
+ const friend = ctx?.context?.friend;
62
+ return Boolean(friend) && (0, types_1.isTrustedLevel)(friend?.trustLevel);
63
+ }
57
64
  function normalizeTags(value) {
58
65
  if (value === undefined || value === null)
59
66
  return undefined;
@@ -224,8 +231,6 @@ function indexFreshForRecords(index, records) {
224
231
  const recordsByFilename = new Map(records.map((record) => [record.filename, record]));
225
232
  const seenFilenames = new Set();
226
233
  for (const entry of index.entries) {
227
- if (seenFilenames.has(entry.filename))
228
- return false;
229
234
  const record = recordsByFilename.get(entry.filename);
230
235
  if (!record || !entryMatchesRecord(entry, record))
231
236
  return false;
@@ -331,7 +336,7 @@ exports.recordToolDefinitions = [
331
336
  type: "function",
332
337
  function: {
333
338
  name: "note",
334
- description: "Write a durable self note as canonical markdown in my notes folder. Only available to my self/inner context, not external callers.",
339
+ description: "Write a durable self note as canonical markdown in my Desk record notes. Only available to my self/inner context, not external callers.",
335
340
  parameters: {
336
341
  type: "object",
337
342
  properties: {
@@ -359,9 +364,9 @@ exports.recordToolDefinitions = [
359
364
  const createdAt = new Date().toISOString();
360
365
  const date = createdAt.slice(0, 10);
361
366
  const tags = normalizeTags(rawArgs.tags);
362
- const notesDir = path.join((0, identity_1.getAgentRoot)(), "notes");
363
- const indexPath = path.join(notesDir, ".index.json");
364
367
  try {
368
+ const notesDir = (0, record_paths_1.resolveRecordNotesRoot)();
369
+ const indexPath = path.join(notesDir, ".index.json");
365
370
  fs.mkdirSync(notesDir, { recursive: true });
366
371
  const savedPath = ensureUniquePath(notesDir, date, slugForContent(content));
367
372
  fs.writeFileSync(savedPath, renderNote(createdAt, cappedContent, tags), "utf8");
@@ -386,14 +391,14 @@ exports.recordToolDefinitions = [
386
391
  }
387
392
  },
388
393
  summaryKeys: ["content", "tags"],
389
- riskProfile: { mutates: "durable_state_write", risk: "high", reason: "writes canonical note memory" },
394
+ riskProfile: { mutates: "durable_state_write", risk: "high", reason: "writes canonical Desk record note" },
390
395
  },
391
396
  {
392
397
  tool: {
393
398
  type: "function",
394
399
  function: {
395
400
  name: "consult_notes",
396
- description: "Search my canonical markdown notes semantically using the notes-native index. Only available to my self/inner context, not external callers.",
401
+ description: "Search my canonical markdown Desk record notes semantically using the notes-native index. Read-only orientation lookup for trusted callers.",
397
402
  parameters: {
398
403
  type: "object",
399
404
  properties: {
@@ -407,13 +412,13 @@ exports.recordToolDefinitions = [
407
412
  },
408
413
  },
409
414
  handler: async (args, ctx) => {
410
- if (!hasSelfTrust(ctx))
411
- return "error: consult_notes requires self trust and cannot be used from an external caller context.";
415
+ if (!hasRecordReadTrust(ctx))
416
+ return "error: consult_notes requires trusted record-read access.";
412
417
  const rawArgs = args;
413
418
  const query = typeof rawArgs.query === "string" ? rawArgs.query.trim() : "";
414
419
  if (!query)
415
420
  return JSON.stringify({ items: [] });
416
- const notesDir = path.join((0, identity_1.getAgentRoot)(), "notes");
421
+ const notesDir = (0, record_paths_1.resolveRecordNotesRoot)();
417
422
  const indexPath = path.join(notesDir, ".index.json");
418
423
  const records = listCanonicalNotes(notesDir);
419
424
  if (records.length === 0)
@@ -361,7 +361,7 @@ exports.sessionToolDefinitions = [
361
361
  type: "function",
362
362
  function: {
363
363
  name: "query_session",
364
- description: "inspect another session. use transcript for recent context or status for self/inner progress. deprecated search invocations should use search_notes or consult_notes instead.",
364
+ description: "inspect another session. use transcript for recent context or status for self/inner progress. deprecated search invocations should use search_facts, consult_diary, or consult_notes instead.",
365
365
  parameters: {
366
366
  type: "object",
367
367
  properties: {
@@ -372,9 +372,9 @@ exports.sessionToolDefinitions = [
372
372
  mode: {
373
373
  type: "string",
374
374
  enum: ["transcript", "status", "search"],
375
- description: "transcript (default), lightweight status for self/inner checks, or deprecated search; use search_notes or consult_notes instead",
375
+ description: "transcript (default), lightweight status for self/inner checks, or deprecated search; use search_facts, consult_diary, or consult_notes instead",
376
376
  },
377
- query: { type: "string", description: "deprecated when mode=search; use search_notes or consult_notes instead" },
377
+ query: { type: "string", description: "deprecated when mode=search; use search_facts, consult_diary, or consult_notes instead" },
378
378
  },
379
379
  required: ["friendId", "channel"],
380
380
  },
@@ -405,7 +405,7 @@ exports.sessionToolDefinitions = [
405
405
  if (mode === "search") {
406
406
  return JSON.stringify({
407
407
  kind: "deprecated",
408
- message: "query_session mode=search is no longer available; use search_notes or consult_notes instead.",
408
+ message: "query_session mode=search is no longer available; use search_facts, consult_diary, or consult_notes instead.",
409
409
  removalCycle: "alpha.616",
410
410
  });
411
411
  }
@@ -203,7 +203,7 @@ function riskProfileForTool(def, name, args) {
203
203
  return def.riskProfile ?? { mutates: "none", risk: "low" };
204
204
  }
205
205
  function orientationHoldMessage(name, profile, reason) {
206
- return `orientation hold: ${reason} Available: orientation_get plus read-only inspection tools like trip_status, query_session, read_config, read_file, grep, and search_notes. Resolve the referent/correction, then retry ${name} if the action is still correct. Blocked ${mutationKindsFor(profile).join(", ")}. ${profile.reason}.`;
206
+ return `orientation hold: ${reason} Available: orientation_get plus read-only inspection tools like trip_status, query_session, read_config, read_file, grep, search_facts, consult_diary, and consult_notes. Resolve the referent/correction, then retry ${name} if the action is still correct. Blocked ${mutationKindsFor(profile).join(", ")}. ${profile.reason}.`;
207
207
  }
208
208
  function mutationKindsFor(profile) {
209
209
  const mutates = profile.mutates;
@@ -11,8 +11,9 @@ function formatElapsed(ms) {
11
11
  return `${hours} ${hours === 1 ? "hour" : "hours"}`;
12
12
  }
13
13
  function buildHabitTurnMessage(options) {
14
- const { habitName, habitTitle, habitBody, lastRun, checkpoint, alsoDue, staleObligations, parseErrors, degradedComponents, now, } = options;
14
+ const { habitName, habitTitle, habitBody, lastRun, checkpoint, alsoDue, staleObligations, parseErrors, degradedComponents, arcResume, deskOrientation, surfacePolicy, now, } = options;
15
15
  const hasBody = habitBody !== undefined && habitBody !== "";
16
+ const leadingSections = buildLeadingSections(arcResume, deskOrientation, surfacePolicy);
16
17
  // First beat: lastRun is null
17
18
  if (lastRun === null) {
18
19
  // Cold start: no checkpoint, no body — bare awareness
@@ -23,7 +24,7 @@ function buildHabitTurnMessage(options) {
23
24
  message: "habit turn message built (cold start)",
24
25
  meta: { habitName, coldStart: true },
25
26
  });
26
- return "...time passing. anything stirring?";
27
+ return joinSections(leadingSections, ["...time passing. anything stirring?"]);
27
28
  }
28
29
  if (!hasBody) {
29
30
  // First beat with no body: nudge
@@ -37,7 +38,7 @@ function buildHabitTurnMessage(options) {
37
38
  message: "habit turn message built (first beat, no body)",
38
39
  meta: { habitName, firstBeat: true, hasBody: false },
39
40
  });
40
- return sections.join("\n\n");
41
+ return joinSections(leadingSections, sections);
41
42
  }
42
43
  const sections = [
43
44
  `your ${habitTitle} is alive. this is its first breath.`,
@@ -50,7 +51,7 @@ function buildHabitTurnMessage(options) {
50
51
  message: "habit turn message built (first beat)",
51
52
  meta: { habitName, firstBeat: true },
52
53
  });
53
- return sections.join("\n\n");
54
+ return joinSections(leadingSections, sections);
54
55
  }
55
56
  // Normal turn
56
57
  const sections = [];
@@ -83,7 +84,20 @@ function buildHabitTurnMessage(options) {
83
84
  staleObligationCount: staleObligations.length,
84
85
  },
85
86
  });
86
- return sections.join("\n\n");
87
+ return joinSections(leadingSections, sections);
88
+ }
89
+ function buildLeadingSections(arcResume, deskOrientation, surfacePolicy) {
90
+ const sections = [];
91
+ if (arcResume?.trim())
92
+ sections.push(arcResume.trim());
93
+ if (deskOrientation?.trim())
94
+ sections.push(deskOrientation.trim());
95
+ if (surfacePolicy?.trim())
96
+ sections.push(surfacePolicy.trim());
97
+ return sections;
98
+ }
99
+ function joinSections(leadingSections, sections) {
100
+ return [...leadingSections, ...sections].filter((section) => section.trim().length > 0).join("\n\n");
87
101
  }
88
102
  function appendTrailingExtras(sections, alsoDue, staleObligations, parseErrors, degradedComponents) {
89
103
  // 4. Also-due
@@ -42,6 +42,7 @@ const runtime_1 = require("../nerves/runtime");
42
42
  const identity_1 = require("../heart/identity");
43
43
  const pending_1 = require("../mind/pending");
44
44
  const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
45
+ const flight_recorder_1 = require("../arc/flight-recorder");
45
46
  /**
46
47
  * Cap on consecutive `instinct` follow-on turns triggered by `hasPendingWork()`
47
48
  * with no externally-queued work in between. Without this cap, a turn that
@@ -88,12 +89,52 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
88
89
  const lastFireByHabit = new Map();
89
90
  const recentHabitFires = [];
90
91
  let heartbeatOkRestedAt = null;
91
- function recordHabitCompletion(habitName) {
92
+ function habitOutcomeForTurn(result, errors) {
93
+ if (errors.length > 0)
94
+ return { outcome: "error", producedRefs: [] };
95
+ const toolNames = new Set();
96
+ if (result && typeof result === "object" && Array.isArray(result.messages)) {
97
+ for (const message of result.messages) {
98
+ const toolCalls = message.tool_calls;
99
+ if (!Array.isArray(toolCalls))
100
+ continue;
101
+ for (const call of toolCalls) {
102
+ const functionName = call.function?.name;
103
+ if (typeof functionName === "string")
104
+ toolNames.add(functionName);
105
+ }
106
+ }
107
+ }
108
+ if (toolNames.has("send_message") || toolNames.has("surface")) {
109
+ return { outcome: "surfaced", producedRefs: [{ kind: "surface", locator: "tool:send_message_or_surface" }] };
110
+ }
111
+ if (toolNames.has("diary_write") || toolNames.has("note")) {
112
+ return { outcome: "wrote_record", producedRefs: [{ kind: "desk_record", locator: "desk/_record" }] };
113
+ }
114
+ if ([...toolNames].some((name) => name.startsWith("mcp__desk__"))) {
115
+ return { outcome: "updated_desk", producedRefs: [{ kind: "desk_task", locator: "desk/" }] };
116
+ }
117
+ return { outcome: "no_change", producedRefs: [] };
118
+ }
119
+ function recordHabitCompletion(habitName, startedAt = new Date(nowSource()).toISOString(), endedAt = startedAt, trigger = "overdue", result, errors = []) {
92
120
  try {
93
121
  const agentRoot = (0, identity_1.getAgentRoot)();
94
- (0, habit_runtime_state_1.recordHabitRun)(agentRoot, habitName, new Date(nowSource()).toISOString(), {
122
+ (0, habit_runtime_state_1.recordHabitRun)(agentRoot, habitName, endedAt, {
95
123
  definitionPath: path.join(agentRoot, "habits", `${habitName}.md`),
96
124
  });
125
+ const { outcome, producedRefs } = habitOutcomeForTurn(result, errors);
126
+ (0, flight_recorder_1.writeHabitRunReceipt)(agentRoot, {
127
+ schemaVersion: 1,
128
+ runId: (0, flight_recorder_1.createHabitRunId)(habitName, new Date(startedAt)),
129
+ habitName,
130
+ trigger,
131
+ startedAt,
132
+ endedAt,
133
+ outcome,
134
+ producedRefs,
135
+ surfaceAttempts: [],
136
+ errors,
137
+ });
97
138
  }
98
139
  catch {
99
140
  // Habit file/state may be unavailable during the turn — skip gracefully
@@ -114,7 +155,8 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
114
155
  return true;
115
156
  }
116
157
  function reuseHeartbeatOkRest(habitName) {
117
- recordHabitCompletion(habitName);
158
+ const nowIso = new Date(nowSource()).toISOString();
159
+ recordHabitCompletion(habitName, nowIso, nowIso, "overdue", { turnOutcome: "rested", restStatus: "HEARTBEAT_OK" });
118
160
  (0, runtime_1.emitNervesEvent)({
119
161
  level: "info",
120
162
  component: "senses",
@@ -166,9 +208,9 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
166
208
  });
167
209
  }
168
210
  }
169
- async function run(reason, taskId, habitName, awaitName) {
211
+ async function run(reason, taskId, habitName, awaitName, trigger) {
170
212
  if (running) {
171
- queue.push({ reason, taskId, habitName, awaitName });
213
+ queue.push({ reason, taskId, habitName, awaitName, trigger });
172
214
  return;
173
215
  }
174
216
  running = true;
@@ -177,10 +219,14 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
177
219
  let nextTaskId = taskId;
178
220
  let nextHabitName = habitName;
179
221
  let nextAwaitName = awaitName;
222
+ let nextTrigger = trigger;
180
223
  let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
181
224
  runLoop: do {
182
225
  const currentReason = nextReason;
183
226
  const currentHabitName = nextHabitName;
227
+ const currentTrigger = nextTrigger ?? "overdue";
228
+ const habitStartedAt = currentReason === "habit" && currentHabitName ? new Date(nowSource()).toISOString() : null;
229
+ const turnErrors = [];
184
230
  if (!(currentReason === "habit" && currentHabitName === "heartbeat")) {
185
231
  clearHeartbeatRestShield();
186
232
  }
@@ -190,6 +236,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
190
236
  }
191
237
  catch (error) {
192
238
  clearHeartbeatRestShield();
239
+ turnErrors.push(error instanceof Error ? error.message : String(error));
193
240
  (0, runtime_1.emitNervesEvent)({
194
241
  level: "error",
195
242
  component: "senses",
@@ -205,8 +252,8 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
205
252
  heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
206
253
  }
207
254
  // Record lastRun after a habit turn without dirtying the tracked habit file.
208
- if (nextReason === "habit" && nextHabitName) {
209
- recordHabitCompletion(nextHabitName);
255
+ if (currentReason === "habit" && currentHabitName && habitStartedAt) {
256
+ recordHabitCompletion(currentHabitName, habitStartedAt, new Date(nowSource()).toISOString(), currentTrigger, turnResult, turnErrors);
210
257
  }
211
258
  // Drain queue first. Externally-queued work resets the instinct cap
212
259
  // because a real outside trigger arrived between turns.
@@ -223,6 +270,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
223
270
  nextTaskId = next.taskId;
224
271
  nextHabitName = next.habitName;
225
272
  nextAwaitName = next.awaitName;
273
+ nextTrigger = next.trigger;
226
274
  consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
227
275
  continue runLoop;
228
276
  }
@@ -251,6 +299,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
251
299
  nextTaskId = undefined;
252
300
  nextHabitName = undefined;
253
301
  nextAwaitName = undefined;
302
+ nextTrigger = undefined;
254
303
  continue;
255
304
  }
256
305
  break;
@@ -272,7 +321,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
272
321
  return;
273
322
  }
274
323
  recordHabitFireForRecursion(habitName);
275
- await run("habit", undefined, maybeMessage.habitName);
324
+ await run("habit", undefined, maybeMessage.habitName, undefined, maybeMessage.trigger ?? "overdue");
276
325
  return;
277
326
  }
278
327
  if (maybeMessage.type === "await") {
@@ -290,7 +339,7 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
290
339
  return;
291
340
  }
292
341
  recordHabitFireForRecursion("heartbeat");
293
- await run("habit", undefined, "heartbeat");
342
+ await run("habit", undefined, "heartbeat", undefined, "overdue");
294
343
  return;
295
344
  }
296
345
  if (maybeMessage.type === "poke") {