atris 3.30.0 → 3.30.2

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 (69) hide show
  1. package/AGENTS.md +6 -0
  2. package/atris/AGENTS.md +11 -0
  3. package/atris/CLAUDE.md +5 -0
  4. package/atris/atris.md +19 -4
  5. package/atris/policies/atris-design.md +71 -0
  6. package/atris/policies/design-seed.md +187 -0
  7. package/atris/skills/atris/SKILL.md +26 -3
  8. package/atris/skills/design/SKILL.md +3 -2
  9. package/atris/skills/loop/SKILL.md +5 -3
  10. package/atris/team/_template/MEMBER.md +19 -0
  11. package/atris.md +63 -23
  12. package/ax +1434 -1698
  13. package/bin/atris.js +5 -1
  14. package/commands/agent-spawn.js +2 -2
  15. package/commands/brain.js +92 -7
  16. package/commands/brainstorm.js +62 -22
  17. package/commands/business-sync.js +92 -8
  18. package/commands/business.js +13 -7
  19. package/commands/chat-scan.js +102 -0
  20. package/commands/computer.js +758 -15
  21. package/commands/deck.js +505 -105
  22. package/commands/init.js +6 -1
  23. package/commands/launchpad.js +638 -0
  24. package/commands/log-sync.js +44 -13
  25. package/commands/loop-front.js +290 -0
  26. package/commands/member.js +124 -3
  27. package/commands/mission.js +653 -30
  28. package/commands/pull.js +37 -28
  29. package/commands/pulse.js +11 -8
  30. package/commands/run.js +79 -39
  31. package/commands/sync.js +36 -17
  32. package/commands/task.js +1072 -89
  33. package/commands/workflow.js +6 -6
  34. package/commands/xp.js +13 -1
  35. package/commands/youtube.js +221 -7
  36. package/decks/README.md +89 -0
  37. package/decks/archetype-catalog.json +180 -0
  38. package/decks/atris-antislop-pitch.json +48 -0
  39. package/decks/atris-archetypes-v2.json +83 -0
  40. package/decks/atris-one-loop-pitch.json +80 -0
  41. package/decks/atris-seed-pitch-v3.json +118 -0
  42. package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
  43. package/decks/atris-seed-pitch-v5.json +109 -0
  44. package/decks/atris-seed-pitch-v6.json +137 -0
  45. package/decks/atris-seed-pitch-v7.json +133 -0
  46. package/decks/atris-single-shot-proof.json +74 -0
  47. package/decks/mark-pincus-narrative.json +102 -0
  48. package/decks/mark-pincus-sourcery.json +94 -0
  49. package/decks/yash-applied-compute-detailed.json +150 -0
  50. package/decks/yash-applied-compute-generalist.json +82 -0
  51. package/decks/yash-applied-compute-narrative.json +54 -0
  52. package/lib/ax-prefs.js +5 -12
  53. package/lib/chat-log-scan.js +377 -0
  54. package/lib/context-gatherer.js +35 -1
  55. package/lib/deck-compose.js +145 -0
  56. package/lib/deck-history.js +64 -0
  57. package/lib/deck-layout.js +169 -0
  58. package/lib/deck-review.js +431 -0
  59. package/lib/deck-schema.js +154 -0
  60. package/lib/file-ops.js +3 -3
  61. package/lib/functional-owner.js +189 -0
  62. package/lib/journal.js +7 -73
  63. package/lib/slides-deck.js +512 -58
  64. package/lib/task-db.js +128 -11
  65. package/package.json +2 -1
  66. package/templates/business-starter/team/START_HERE.md +12 -8
  67. package/utils/auth.js +4 -0
  68. package/utils/config.js +4 -0
  69. package/atris/atrisDev.md +0 -717
@@ -0,0 +1,154 @@
1
+ // Deck spec schema validation — catch a bad spec before it hits the Slides API.
2
+ //
3
+ // lintSpec (lib/deck-review.js) judges *taste* (clip risk, template fatigue,
4
+ // AI-tell copy). validateSpec judges *shape*: unknown slide types, missing
5
+ // required fields, bad theme, malformed arrays. It runs on `deck lint` and
6
+ // gates `deck build`, so a typo fails locally with a slide index instead of a
7
+ // cryptic batch-update error after the network round trip.
8
+ //
9
+ // Findings share lintSpec's shape: { severity, slide, rule, message } where
10
+ // slide 0 is a spec-level finding and slide N (1-based) points at a slide.
11
+
12
+ const { THEMES, ARCHE } = require('./slides-deck');
13
+
14
+ const KNOWN_THEMES = new Set(Object.keys(THEMES));
15
+
16
+ // Per-type contract.
17
+ // need: list of alternative-groups; each group needs >=1 field with content.
18
+ // arr: fields that, when present, must be non-empty arrays.
19
+ // Keep this in lockstep with ARCHE in slides-deck.js (a drift test enforces it).
20
+ const SLIDE_SCHEMA = {
21
+ title: { need: [['headline', 'title']], arr: {} },
22
+ statement: { need: [['text', 'headline']], arr: {} },
23
+ columns: { need: [['columns']], arr: { columns: { min: 1 } } },
24
+ panel: { need: [['panel']], arr: {} },
25
+ chips: { need: [['chips']], arr: { chips: { min: 1 } } },
26
+ bignumber: { need: [['number', 'value']], arr: {} },
27
+ close: { need: [], arr: { buttons: { min: 0 } } },
28
+ timeline: { need: [['steps']], arr: { steps: { min: 1 } } },
29
+ versus: { need: [['left'], ['right']], arr: {} },
30
+ metricgrid: { need: [['metrics']], arr: { metrics: { min: 1 } } },
31
+ receipt: { need: [['fields']], arr: { fields: { min: 1 } } },
32
+ stack: { need: [['layers']], arr: { layers: { min: 1 } } },
33
+ quote: { need: [['text']], arr: {} },
34
+ hero: { need: [['headline', 'text']], arr: {} },
35
+ interstitial: { need: [['text', 'headline']], arr: {} },
36
+ lede: { need: [['headline', 'title']], arr: {} },
37
+ prose: { need: [['paragraphs', 'body']], arr: { paragraphs: { min: 0 } } },
38
+ split: { need: [['body', 'text']], arr: {} },
39
+ bullets: { need: [['items']], arr: { items: { min: 1 } } },
40
+ };
41
+
42
+ function hasContent(v) {
43
+ if (v == null) return false;
44
+ if (typeof v === 'string') return v.trim().length > 0;
45
+ if (Array.isArray(v)) return v.length > 0;
46
+ if (typeof v === 'object') return Object.keys(v).length > 0;
47
+ return Boolean(v) || v === 0;
48
+ }
49
+
50
+ function validateSpec(spec) {
51
+ const findings = [];
52
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
53
+ findings.push({ severity: 'error', slide: 0, rule: 'spec-shape', message: 'Spec must be a JSON object' });
54
+ return findings;
55
+ }
56
+ if (spec.theme != null && !KNOWN_THEMES.has(spec.theme)) {
57
+ findings.push({
58
+ severity: 'error',
59
+ slide: 0,
60
+ rule: 'unknown-theme',
61
+ message: `Unknown theme "${spec.theme}". Use: ${[...KNOWN_THEMES].join(', ')}`,
62
+ });
63
+ }
64
+ const slides = spec.slides;
65
+ if (!Array.isArray(slides) || slides.length === 0) {
66
+ findings.push({ severity: 'error', slide: 0, rule: 'no-slides', message: 'Spec needs a non-empty "slides" array' });
67
+ return findings;
68
+ }
69
+ slides.forEach((slide, i) => {
70
+ const n = i + 1;
71
+ if (!slide || typeof slide !== 'object' || Array.isArray(slide)) {
72
+ findings.push({ severity: 'error', slide: n, rule: 'slide-shape', message: 'Slide must be an object' });
73
+ return;
74
+ }
75
+ const type = slide.type;
76
+ if (!hasContent(type)) {
77
+ findings.push({ severity: 'error', slide: n, rule: 'missing-type', message: 'Slide is missing a "type"' });
78
+ return;
79
+ }
80
+ const def = SLIDE_SCHEMA[type];
81
+ if (!def) {
82
+ findings.push({
83
+ severity: 'error',
84
+ slide: n,
85
+ rule: 'unknown-slide-type',
86
+ message: `Unknown slide type "${type}". Known: ${Object.keys(SLIDE_SCHEMA).join(', ')}`,
87
+ });
88
+ return;
89
+ }
90
+ (def.need || []).forEach((alts) => {
91
+ if (!alts.some((k) => hasContent(slide[k]))) {
92
+ const label = alts.map((a) => `"${a}"`).join(' or ');
93
+ findings.push({ severity: 'error', slide: n, rule: 'missing-field', message: `"${type}" slide needs ${label}` });
94
+ }
95
+ });
96
+ Object.entries(def.arr || {}).forEach(([field, ruleDef]) => {
97
+ const v = slide[field];
98
+ if (v == null) return;
99
+ if (!Array.isArray(v)) {
100
+ findings.push({ severity: 'error', slide: n, rule: 'bad-array', message: `Slide field "${field}" must be an array` });
101
+ } else if (ruleDef.min && v.length < ruleDef.min) {
102
+ findings.push({ severity: 'warn', slide: n, rule: 'empty-array', message: `Slide field "${field}" is empty` });
103
+ }
104
+ });
105
+ // Nested arrays the flat `arr` contract can't reach: a non-array here would
106
+ // pass the gate and then crash the renderer, so check shape explicitly.
107
+ if (type === 'panel' && slide.panel && slide.panel.rows != null && !Array.isArray(slide.panel.rows)) {
108
+ findings.push({ severity: 'error', slide: n, rule: 'bad-array', message: 'panel.rows must be an array' });
109
+ }
110
+ if (type === 'versus') {
111
+ for (const side of ['left', 'right']) {
112
+ const items = slide[side] && slide[side].items;
113
+ if (items != null && !Array.isArray(items)) {
114
+ findings.push({ severity: 'error', slide: n, rule: 'bad-array', message: `versus ${side}.items must be an array` });
115
+ }
116
+ }
117
+ }
118
+ });
119
+ return findings;
120
+ }
121
+
122
+ // Order findings for display: spec-level first, then by slide, errors above warns.
123
+ const SEV_RANK = { error: 0, warn: 1, info: 2 };
124
+ function sortFindings(findings) {
125
+ return [...findings].sort((a, b) => {
126
+ if (a.slide !== b.slide) return a.slide - b.slide;
127
+ return (SEV_RANK[a.severity] ?? 3) - (SEV_RANK[b.severity] ?? 3);
128
+ });
129
+ }
130
+
131
+ function hasErrors(findings) {
132
+ return findings.some((f) => f.severity === 'error');
133
+ }
134
+
135
+ // Schema validation (shape) + lint (taste), merged and ordered.
136
+ function checkSpec(spec, lintSpec) {
137
+ const schema = validateSpec(spec);
138
+ // Only run the taste lint when the shape is sane enough to walk slides.
139
+ const taste = !hasErrors(schema.filter((f) => f.rule === 'spec-shape' || f.rule === 'no-slides')) && typeof lintSpec === 'function'
140
+ ? lintSpec(spec)
141
+ : [];
142
+ return sortFindings([...schema, ...taste]);
143
+ }
144
+
145
+ module.exports = {
146
+ KNOWN_THEMES,
147
+ SLIDE_SCHEMA,
148
+ validateSpec,
149
+ sortFindings,
150
+ hasErrors,
151
+ hasContent,
152
+ checkSpec,
153
+ ARCHE_TYPES: Object.keys(ARCHE),
154
+ };
package/lib/file-ops.js CHANGED
@@ -57,7 +57,7 @@ function createLogFile(logFile, dateFormatted) {
57
57
  const prevMonth = String(prev.getMonth() + 1).padStart(2, '0');
58
58
  const prevDay = String(prev.getDate()).padStart(2, '0');
59
59
  const prevDateFormatted = `${prevYear}-${prevMonth}-${prevDay}`;
60
- const prevLogFile = path.join(process.cwd(), 'atris', 'logs', prevYear.toString(), `${prevDateFormatted}.md`);
60
+ const { logFile: prevLogFile } = getLogPath(prevDateFormatted);
61
61
 
62
62
  if (fs.existsSync(prevLogFile)) {
63
63
  const prevContent = fs.readFileSync(prevLogFile, 'utf8');
@@ -105,8 +105,8 @@ function parseInboxItems(content) {
105
105
  if (trimmed.startsWith('(Empty')) return;
106
106
  const parsed = trimmed.match(/^- \*\*I(\d+):\*\*\s*(.+)$|^- \*\*I(\d+):\s+(.+)$/);
107
107
  if (parsed) {
108
- const id = parseInt(parsed[1] || parsed[3], 10);
109
- const text = parsed[2] || parsed[4];
108
+ const id = parseInt(parsed[1] !== undefined ? parsed[1] : parsed[3], 10);
109
+ const text = parsed[2] !== undefined ? parsed[2] : parsed[4];
110
110
  items.push({ id, text, line: trimmed });
111
111
  }
112
112
  });
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const escapeRegExp = require('./escape-regexp');
6
+
7
+ const ENGINE_OWNER_IDS = new Set([
8
+ 'atris-2-fast',
9
+ 'atris2',
10
+ 'atris2-fast',
11
+ 'claude',
12
+ 'codex',
13
+ 'codex-executor',
14
+ 'cursor',
15
+ 'devin',
16
+ 'executor',
17
+ 'openclaw',
18
+ 'windsurf',
19
+ ]);
20
+
21
+ const FUNCTIONAL_MEMBER_TOPICS = [
22
+ {
23
+ owner: 'task-planner',
24
+ terms: ['task', 'plan', 'planner', 'planning', 'preview', 'landing', 'queue', 'backlog', 'owner', 'assignment', 'routing'],
25
+ },
26
+ {
27
+ owner: 'architect',
28
+ terms: ['member', 'team', 'factory', 'architecture', 'workflow', 'role', 'permission', 'policy', 'contract'],
29
+ },
30
+ {
31
+ owner: 'mission-lead',
32
+ terms: ['mission', 'overnight', '24/7', 'autonomy', 'always-on', 'loop', 'heartbeat', 'cadence', 'lease', 'goal'],
33
+ },
34
+ {
35
+ owner: 'validator',
36
+ terms: ['proof', 'review', 'verify', 'verifier', 'validation', 'test', 'check', 'inspect'],
37
+ },
38
+ {
39
+ owner: 'launcher',
40
+ terms: ['release', 'launch', 'publish', 'ship', 'dmg', 'zip', 'post'],
41
+ },
42
+ {
43
+ owner: 'researcher',
44
+ terms: ['research', 'paper', 'source', 'market', 'investigate'],
45
+ },
46
+ {
47
+ owner: 'wiki-miner',
48
+ terms: ['wiki', 'knowledge', 'map', 'docs', 'documentation', 'journal'],
49
+ },
50
+ {
51
+ owner: 'navigator',
52
+ terms: ['navigation', 'where', 'find', 'locate', 'map'],
53
+ },
54
+ {
55
+ owner: 'improver',
56
+ terms: ['improve', 'cleanup', 'clean', 'maintenance', 'tidy', 'stale'],
57
+ },
58
+ {
59
+ owner: 'signal-scout',
60
+ terms: ['signal', 'signals', 'error', 'failure', 'pattern', 'alert', 'watch', 'log', 'logs', 'infer'],
61
+ },
62
+ ];
63
+
64
+ function normalizeOwnerSlug(value) {
65
+ return String(value || '')
66
+ .trim()
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9]+/g, '-')
69
+ .replace(/^-+|-+$/g, '');
70
+ }
71
+
72
+ function isEngineOwner(value) {
73
+ return ENGINE_OWNER_IDS.has(normalizeOwnerSlug(value));
74
+ }
75
+
76
+ function listWorkspaceMemberSlugs(root) {
77
+ const teamDir = path.join(root || process.cwd(), 'atris', 'team');
78
+ try {
79
+ return new Set(fs.readdirSync(teamDir, { withFileTypes: true })
80
+ .filter(entry => entry.isDirectory())
81
+ .map(entry => entry.name)
82
+ .filter(name => name !== '_template')
83
+ .filter(name => fs.existsSync(path.join(teamDir, name, 'MEMBER.md')))
84
+ .map(normalizeOwnerSlug));
85
+ } catch (_) {
86
+ return new Set();
87
+ }
88
+ }
89
+
90
+ function firstAvailableFunctionalOwner(candidates, existingMembers) {
91
+ for (const candidate of candidates) {
92
+ const owner = normalizeOwnerSlug(candidate);
93
+ if (!owner) continue;
94
+ if (existingMembers.size === 0 || existingMembers.has(owner)) return owner;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ function ownerTermScore(text, term, weight = 1) {
100
+ const haystack = String(text || '').toLowerCase();
101
+ const needle = String(term || '').toLowerCase();
102
+ if (!haystack || !needle) return 0;
103
+ if (/^[a-z0-9-]+$/.test(needle)) {
104
+ const re = new RegExp(`(^|[^a-z0-9])${escapeRegExp(needle)}([^a-z0-9]|$)`, 'i');
105
+ return re.test(haystack) ? weight : 0;
106
+ }
107
+ return haystack.includes(needle) ? weight : 0;
108
+ }
109
+
110
+ function proposedMemberSlugFromTask({ title, tag }) {
111
+ const source = normalizeOwnerSlug(tag && tag !== true ? tag : title);
112
+ const core = source
113
+ .split('-')
114
+ .filter(part => part && !['add', 'build', 'create', 'design', 'fix', 'make', 'replace', 'ship', 'the', 'with'].includes(part))
115
+ .slice(0, 3)
116
+ .join('-');
117
+ return `${core || 'feature'}-owner`;
118
+ }
119
+
120
+ function resolveFunctionalOwner({ requestedOwner, title, tag, note, goal, root, fallbackOwners } = {}) {
121
+ const requested = normalizeOwnerSlug(requestedOwner);
122
+ const existingMembers = listWorkspaceMemberSlugs(root);
123
+ const highSignalText = [title, note, goal].filter(Boolean).join(' ').toLowerCase();
124
+ const tagText = tag && tag !== true ? String(tag).toLowerCase() : '';
125
+ const text = [highSignalText, tagText].filter(Boolean).join(' ');
126
+
127
+ if (requested && !isEngineOwner(requested)) {
128
+ return {
129
+ owner: requested,
130
+ reason: 'explicit_functional_owner',
131
+ requested_owner: requested,
132
+ };
133
+ }
134
+
135
+ const exact = firstAvailableFunctionalOwner(
136
+ FUNCTIONAL_MEMBER_TOPICS.filter(topic => text.includes(topic.owner)).map(topic => topic.owner),
137
+ existingMembers
138
+ );
139
+ if (exact) {
140
+ return {
141
+ owner: exact,
142
+ reason: requested ? 'engine_owner_resolved_by_exact_member' : 'inferred_exact_member',
143
+ requested_owner: requested || null,
144
+ executed_by: requested && isEngineOwner(requested) ? requested : null,
145
+ };
146
+ }
147
+
148
+ const scored = FUNCTIONAL_MEMBER_TOPICS
149
+ .map(topic => ({
150
+ owner: topic.owner,
151
+ score: topic.terms.reduce((sum, term) => (
152
+ sum
153
+ + ownerTermScore(highSignalText, term, 3)
154
+ + ownerTermScore(tagText, term, 1)
155
+ ), 0),
156
+ }))
157
+ .filter(row => row.score > 0)
158
+ .sort((a, b) => b.score - a.score);
159
+ const inferred = firstAvailableFunctionalOwner(scored.map(row => row.owner), existingMembers);
160
+ if (inferred) {
161
+ return {
162
+ owner: inferred,
163
+ reason: requested ? 'engine_owner_resolved_by_task_signal' : 'inferred_by_task_signal',
164
+ requested_owner: requested || null,
165
+ executed_by: requested && isEngineOwner(requested) ? requested : null,
166
+ };
167
+ }
168
+
169
+ const fallback = firstAvailableFunctionalOwner(fallbackOwners || ['architect', 'task-planner', 'mission-lead', 'validator'], existingMembers)
170
+ || Array.from(existingMembers).find(owner => !isEngineOwner(owner))
171
+ || 'architect';
172
+ return {
173
+ owner: fallback,
174
+ reason: requested ? 'engine_owner_resolved_to_factory_owner' : 'missing_functional_owner_fit',
175
+ requested_owner: requested || null,
176
+ executed_by: requested && isEngineOwner(requested) ? requested : null,
177
+ proposed_member: proposedMemberSlugFromTask({ title, tag }),
178
+ };
179
+ }
180
+
181
+ module.exports = {
182
+ ENGINE_OWNER_IDS,
183
+ FUNCTIONAL_MEMBER_TOPICS,
184
+ normalizeOwnerSlug,
185
+ isEngineOwner,
186
+ listWorkspaceMemberSlugs,
187
+ proposedMemberSlugFromTask,
188
+ resolveFunctionalOwner,
189
+ };
package/lib/journal.js CHANGED
@@ -3,7 +3,13 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
  const { spawnSync } = require('child_process');
6
- const escapeRegExp = require('./escape-regexp');
6
+
7
+ // Path/directory helpers for atris/logs/<YYYY>/<YYYY-MM-DD>.md live in lib/file-ops.
8
+ // Re-export them here so existing `require('../lib/journal')` consumers
9
+ // (activate, autopilot, brainstorm, run, status, visualize, workflow) keep working
10
+ // without code changes while we collapse the duplicate-truth bug. PR2/3 will
11
+ // re-point consumers directly at lib/file-ops.
12
+ const { getLogPath, ensureLogDirectory, createLogFile } = require('./file-ops');
7
13
 
8
14
  /**
9
15
  * Check if two timestamps are effectively the same (within 5ms).
@@ -189,78 +195,6 @@ function showLogDiff(localPath, remoteContent) {
189
195
  }
190
196
  }
191
197
 
192
- function getLogPath(dateStr) {
193
- const targetDir = path.join(process.cwd(), 'atris');
194
- const date = dateStr ? new Date(dateStr) : new Date();
195
- const year = date.getFullYear();
196
- const month = String(date.getMonth() + 1).padStart(2, '0');
197
- const day = String(date.getDate()).padStart(2, '0');
198
- const dateFormatted = `${year}-${month}-${day}`; // YYYY-MM-DD in local time
199
-
200
- const logsDir = path.join(targetDir, 'logs');
201
- const yearDir = path.join(logsDir, year.toString());
202
- const logFile = path.join(yearDir, `${dateFormatted}.md`);
203
-
204
- return { logsDir, yearDir, logFile, dateFormatted };
205
- }
206
-
207
- function ensureLogDirectory() {
208
- const { logsDir, yearDir } = getLogPath();
209
-
210
- if (!fs.existsSync(logsDir)) {
211
- fs.mkdirSync(logsDir, { recursive: true });
212
- }
213
-
214
- if (!fs.existsSync(yearDir)) {
215
- fs.mkdirSync(yearDir, { recursive: true });
216
- }
217
- }
218
-
219
- function createLogFile(logFile, dateFormatted) {
220
- let carryInProgress = '';
221
- let carryBacklog = '';
222
- let carryInbox = '';
223
-
224
- try {
225
- const [y, m, d] = String(dateFormatted).split('-').map(Number);
226
- if (Number.isFinite(y) && Number.isFinite(m) && Number.isFinite(d)) {
227
- const prev = new Date(y, m - 1, d);
228
- prev.setDate(prev.getDate() - 1);
229
-
230
- const prevYear = prev.getFullYear();
231
- const prevMonth = String(prev.getMonth() + 1).padStart(2, '0');
232
- const prevDay = String(prev.getDate()).padStart(2, '0');
233
- const prevDateFormatted = `${prevYear}-${prevMonth}-${prevDay}`;
234
- const prevLogFile = path.join(process.cwd(), 'atris', 'logs', prevYear.toString(), `${prevDateFormatted}.md`);
235
-
236
- if (fs.existsSync(prevLogFile)) {
237
- const prevContent = fs.readFileSync(prevLogFile, 'utf8');
238
-
239
- const sectionBody = (headingLine) => {
240
- const regex = new RegExp(
241
- `## ${escapeRegExp(headingLine)}\\n([\\s\\S]*?)(?=\\n---|\\n## |$)`
242
- );
243
- const match = prevContent.match(regex);
244
- return match ? match[1].trim() : '';
245
- };
246
-
247
- carryInProgress = sectionBody('In Progress 🔄');
248
- carryBacklog = sectionBody('Backlog');
249
- carryInbox = sectionBody('Inbox');
250
- }
251
- }
252
- } catch {
253
- // Best-effort carry-forward; never block journal creation.
254
- }
255
-
256
- const inProgressBody = carryInProgress ? `${carryInProgress}\n\n` : '';
257
- const backlogBody = carryBacklog ? `${carryBacklog}\n\n` : '';
258
- const inboxBody = carryInbox ? `${carryInbox}\n\n` : '';
259
-
260
- const initialContent = `# Log — ${dateFormatted}\n\n## Handoff\n\n---\n\n## Completed ✅\n\n---\n\n## In Progress 🔄\n\n${inProgressBody}---\n\n## Backlog\n\n${backlogBody}---\n\n## Notes\n\n---\n\n## Inbox\n\n${inboxBody}\n`;
261
- fs.writeFileSync(logFile, initialContent);
262
- }
263
-
264
198
  module.exports = {
265
199
  isSameTimestamp,
266
200
  computeContentHash,