mover-os 4.7.7 → 4.7.8
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.
- package/install.js +688 -68
- package/package.json +15 -3
- package/src/dashboard/build.js +1503 -0
- package/src/dashboard/dashboard.js +276 -0
- package/src/dashboard/index.js +319 -0
- package/src/dashboard/lib/activation-log.js +297 -0
- package/src/dashboard/lib/active-context-parser.js +177 -0
- package/src/dashboard/lib/agent-command.js +93 -0
- package/src/dashboard/lib/agent-detect.js +255 -0
- package/src/dashboard/lib/agent-detector.js +92 -0
- package/src/dashboard/lib/agent-session.js +462 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/auto-learnings-parser.js +183 -0
- package/src/dashboard/lib/cli-usage-parser.js +92 -0
- package/src/dashboard/lib/config-parser.js +109 -0
- package/src/dashboard/lib/connect-recommender.js +131 -0
- package/src/dashboard/lib/correlations-parser.js +231 -0
- package/src/dashboard/lib/daily-note-resolver.js +211 -0
- package/src/dashboard/lib/date-utils.js +35 -0
- package/src/dashboard/lib/distribution-parser.js +78 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +83 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-health.js +170 -0
- package/src/dashboard/lib/engine-writer.js +1685 -0
- package/src/dashboard/lib/execution-plan.js +125 -0
- package/src/dashboard/lib/experiments-parser.js +429 -0
- package/src/dashboard/lib/feed-parser.js +294 -0
- package/src/dashboard/lib/forked-future.js +60 -0
- package/src/dashboard/lib/goal-forecast.js +304 -0
- package/src/dashboard/lib/goals-parser.js +67 -0
- package/src/dashboard/lib/health-protocols-parser.js +133 -0
- package/src/dashboard/lib/hook-activity.js +48 -0
- package/src/dashboard/lib/hook-indexer.js +169 -0
- package/src/dashboard/lib/hourly-activity-parser.js +143 -0
- package/src/dashboard/lib/identity-parser.js +85 -0
- package/src/dashboard/lib/ingestion.js +418 -0
- package/src/dashboard/lib/library-indexer-v2.js +212 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/log-activation.sh +61 -0
- package/src/dashboard/lib/memory-curator.js +97 -0
- package/src/dashboard/lib/memory-gardener.js +177 -0
- package/src/dashboard/lib/memory-gepa.js +102 -0
- package/src/dashboard/lib/memory-index.js +470 -0
- package/src/dashboard/lib/memory-rerank.js +72 -0
- package/src/dashboard/lib/memory-text.js +136 -0
- package/src/dashboard/lib/metrics-log-parser.js +76 -0
- package/src/dashboard/lib/moves-usage-parser.js +184 -0
- package/src/dashboard/lib/onboarding-forge.js +70 -0
- package/src/dashboard/lib/override-outcome-parser.js +68 -0
- package/src/dashboard/lib/override-summary.js +73 -0
- package/src/dashboard/lib/paths.js +192 -0
- package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
- package/src/dashboard/lib/phantom-strategy.js +129 -0
- package/src/dashboard/lib/project-scanner.js +121 -0
- package/src/dashboard/lib/promise-wall.js +88 -0
- package/src/dashboard/lib/record-score.js +173 -0
- package/src/dashboard/lib/redaction.js +140 -0
- package/src/dashboard/lib/refusal-parser.js +44 -0
- package/src/dashboard/lib/regenerate-manifest.js +206 -0
- package/src/dashboard/lib/rewind-snapshots.js +689 -0
- package/src/dashboard/lib/roast-wall-parser.js +200 -0
- package/src/dashboard/lib/run-registry.js +226 -0
- package/src/dashboard/lib/safe-write.js +63 -0
- package/src/dashboard/lib/session-log-parser.js +145 -0
- package/src/dashboard/lib/session-time-parser.js +158 -0
- package/src/dashboard/lib/skill-index.js +171 -0
- package/src/dashboard/lib/skill-indexer.js +118 -0
- package/src/dashboard/lib/skill-recommender.js +689 -0
- package/src/dashboard/lib/strategy-parser.js +245 -0
- package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
- package/src/dashboard/lib/streak-parser.js +95 -0
- package/src/dashboard/lib/suggested-now.js +254 -0
- package/src/dashboard/lib/tool-awareness.js +125 -0
- package/src/dashboard/lib/transcript-parser.js +331 -0
- package/src/dashboard/lib/vault-graph-parser.js +205 -0
- package/src/dashboard/lib/view-generator.js +163 -0
- package/src/dashboard/lib/voice-dna-parser.js +57 -0
- package/src/dashboard/lib/walkthrough-script.js +140 -0
- package/src/dashboard/lib/workflow-graph-parser.js +170 -0
- package/src/dashboard/lib/workflow-library-parser.js +146 -0
- package/src/dashboard/server.js +2024 -0
- package/src/dashboard/shortcut.js +0 -0
- package/src/dashboard/static/setup-poc.html +306 -0
- package/src/dashboard/static/walkthrough-poc.html +580 -0
- package/src/dashboard/styles.css +1201 -0
- package/src/dashboard/templates/index.html +278 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hero.png +0 -0
- package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
- package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
- package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
- package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
- package/src/dashboard/ui/dist/icon.svg +4 -0
- package/src/dashboard/ui/dist/index.html +18 -0
- package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
- package/src/dashboard/ui/dist/registerSW.js +1 -0
- package/src/dashboard/ui/dist/sw.js +1 -0
- package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
|
@@ -0,0 +1,1685 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* engine-writer.js — synthesizes a complete Mover OS Engine from a setup-
|
|
5
|
+
* interview transcript, writing Identity_Prime.md, Strategy.md,
|
|
6
|
+
* Active_Context.md, Goals.md, and the first Daily Note.
|
|
7
|
+
*
|
|
8
|
+
* SAFETY CONTRACT (non-negotiable):
|
|
9
|
+
* - Default: dryRun=true — builds file objects in memory, writes NOTHING.
|
|
10
|
+
* - targetDir MUST resolve inside dev/ or a sandbox, never 02_Areas/Engine/.
|
|
11
|
+
* Any attempt to target 02_Areas/Engine/ throws immediately.
|
|
12
|
+
* - Real writes require: dryRun=false AND force=true WHEN targetDir already
|
|
13
|
+
* holds Engine content, else throws.
|
|
14
|
+
* - All writes are atomic (tmp + rename) to avoid partial-file reads.
|
|
15
|
+
*
|
|
16
|
+
* v1 DESIGN: fully deterministic, no LLM spawn. The interview transcript
|
|
17
|
+
* Q&A is parsed by heuristic tag extractors and embedded verbatim under the
|
|
18
|
+
* correct Engine headings. This is intentionally conservative — a future
|
|
19
|
+
* "scribe" LLM agent (see DEFERRED in INTEGRATION.md) will polish the prose.
|
|
20
|
+
* The contract is "real content, never placeholder" — every extracted value is
|
|
21
|
+
* a direct quote from the user, not a template bracket.
|
|
22
|
+
*
|
|
23
|
+
* API:
|
|
24
|
+
* writeEngine({ transcript, digest?, targetDir, dryRun=true, force=false })
|
|
25
|
+
* -> { files: [{path, content}], written: bool }
|
|
26
|
+
*
|
|
27
|
+
* transcript: AgentSession transcript array: [{ role, text }, ...]
|
|
28
|
+
* digest: optional note-ingest digest string (from ingestion.js) for
|
|
29
|
+
* extra context — currently appended to Active_Context § 7.
|
|
30
|
+
* targetDir: absolute path where Engine files will land.
|
|
31
|
+
* dryRun: when true (DEFAULT), returns proposed content without writing.
|
|
32
|
+
* force: when false (DEFAULT), refuses to write into a targetDir that
|
|
33
|
+
* already holds Engine content (non-empty Identity_Prime.md).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require("fs");
|
|
37
|
+
const path = require("path");
|
|
38
|
+
const os = require("os");
|
|
39
|
+
|
|
40
|
+
// ─── Safety gate ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
// Absolute path of the real Engine — WRITES HERE ARE ALWAYS REFUSED.
|
|
43
|
+
// We resolve the home-relative version at module load so it's computed once.
|
|
44
|
+
// On a real vault this will be something like:
|
|
45
|
+
// <vault>/02_Areas/Engine
|
|
46
|
+
// We detect it by checking if targetDir ends with or contains the canonical
|
|
47
|
+
// Engine folder name. Belt + suspenders: also block if it is an exact prefix
|
|
48
|
+
// of the known realpath on this machine.
|
|
49
|
+
const ENGINE_DIR_REL = path.join("02_Areas", "Engine");
|
|
50
|
+
|
|
51
|
+
function assertSafeTarget(targetDir, allowEngineWrite = false) {
|
|
52
|
+
const resolved = path.resolve(targetDir);
|
|
53
|
+
// Compare case-INSENSITIVELY (Bug6): macOS/APFS is case-insensitive, so a lowercase
|
|
54
|
+
// ".../02_areas/engine" points at the real Engine but a case-sensitive endsWith would miss it and
|
|
55
|
+
// let the write through. Keep `resolved` (original case) for the error messages.
|
|
56
|
+
const rLow = resolved.toLowerCase(), eLow = ENGINE_DIR_REL.toLowerCase();
|
|
57
|
+
const isEngineDir = rLow.endsWith(eLow); // the Engine dir itself
|
|
58
|
+
const insideEngine = isEngineDir || rLow.includes(eLow + path.sep); // it or a subdir of it
|
|
59
|
+
if (allowEngineWrite) {
|
|
60
|
+
// allowEngineWrite is NOT a blanket bypass. The onboarding commit must target
|
|
61
|
+
// an actual .../02_Areas/Engine directory — the SERVER resolves this from the
|
|
62
|
+
// vault path; the client cannot supply it. Anything else is a bug or an
|
|
63
|
+
// attack, so fail closed.
|
|
64
|
+
if (!isEngineDir) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[engine-writer] SAFETY BLOCK: allowEngineWrite requires a target ending in 02_Areas/Engine, got ${resolved}.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (insideEngine) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[engine-writer] SAFETY BLOCK: targetDir resolves to or within 02_Areas/Engine/ (${resolved}). ` +
|
|
74
|
+
`This path contains irreplaceable user data. Write to a sandbox instead (e.g. dev/research/.../sandbox-engine/).`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Transcript parser ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pair Q&A turns from the raw transcript.
|
|
83
|
+
* The agent alternates: assistant (question) → user (answer).
|
|
84
|
+
* We number exchanges 1..N from the assistant's first substantive turn.
|
|
85
|
+
* Returns [{ q, a }] — questions are assistant text, answers are user text.
|
|
86
|
+
*/
|
|
87
|
+
function pairTurns(transcript) {
|
|
88
|
+
if (!Array.isArray(transcript) || transcript.length === 0) return [];
|
|
89
|
+
const pairs = [];
|
|
90
|
+
let pending = null;
|
|
91
|
+
for (const t of transcript) {
|
|
92
|
+
if (!t || !t.role || !t.text) continue;
|
|
93
|
+
if (t.role === "assistant") {
|
|
94
|
+
// If there's an unanswered assistant turn, start a new Q without an A
|
|
95
|
+
if (pending !== null) pairs.push({ q: pending, a: "(no answer recorded)" });
|
|
96
|
+
// Don't capture the [[SETUP_COMPLETE]] paragraph as a Q — it's a close.
|
|
97
|
+
const text = t.text.trim();
|
|
98
|
+
if (text.includes("[[SETUP_COMPLETE]]")) {
|
|
99
|
+
// The value prophecy: store separately, not as a Q
|
|
100
|
+
pending = null;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
pending = text;
|
|
104
|
+
} else if (t.role === "user" && pending !== null) {
|
|
105
|
+
pairs.push({ q: pending, a: t.text.trim() });
|
|
106
|
+
pending = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Flush any trailing unanswered question
|
|
110
|
+
if (pending !== null) pairs.push({ q: pending, a: "(unanswered)" });
|
|
111
|
+
return pairs;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract the value prophecy — the 2-3 sentences the agent emits AFTER
|
|
116
|
+
* [[SETUP_COMPLETE]] on its final turn.
|
|
117
|
+
*/
|
|
118
|
+
function extractProphecy(transcript) {
|
|
119
|
+
for (const t of transcript) {
|
|
120
|
+
if (t && t.role === "assistant" && t.text && t.text.includes("[[SETUP_COMPLETE]]")) {
|
|
121
|
+
// Strip the marker AND any structured profile fence — the prophecy is the
|
|
122
|
+
// human-facing sentences only, never the machine block.
|
|
123
|
+
// Sanitize like a single profile value (Bug5): the prophecy is interpolated INLINE into
|
|
124
|
+
// Active_Context.md + the daily note (both parsed section-by-heading), so collapse newlines so
|
|
125
|
+
// it can't open a new block, neutralize "<", and de-fang a leading structural marker.
|
|
126
|
+
return t.text
|
|
127
|
+
.replace(/\[\[SETUP_COMPLETE\]\]/g, "")
|
|
128
|
+
.replace(/```(?:json)?[\s\S]*?```/gi, "")
|
|
129
|
+
.replace(/\s*\n+\s*/g, " ")
|
|
130
|
+
.replace(/</g, "<")
|
|
131
|
+
.trim()
|
|
132
|
+
.replace(/^([!#>|*+\-~=_`])/, "\\$1");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The setup interviewer emits a fenced ```json block — its OWN structured
|
|
140
|
+
* extraction of the profile — on its final turn. Parsing the agent's structured
|
|
141
|
+
* output is far more reliable than keyword-matching question text, so this takes
|
|
142
|
+
* precedence over the heuristic field map. Returns the parsed object, or null
|
|
143
|
+
* (caller falls back to the heuristic) if absent or unparseable.
|
|
144
|
+
*/
|
|
145
|
+
function parseProfile(transcript) {
|
|
146
|
+
if (!Array.isArray(transcript)) return null;
|
|
147
|
+
// SECURITY: the profile is emitted ONLY on the agent's final turn — the one
|
|
148
|
+
// bearing [[SETUP_COMPLETE]]. Scope parsing to THAT turn so an earlier turn
|
|
149
|
+
// that echoes imported-note content can never smuggle a fake mover-profile
|
|
150
|
+
// block into the Engine, even when the genuine final block is malformed.
|
|
151
|
+
// Also require the explicit _kind marker, so a stray JSON object is not
|
|
152
|
+
// mistaken for a profile.
|
|
153
|
+
let finalTurn = null;
|
|
154
|
+
for (let i = transcript.length - 1; i >= 0; i--) {
|
|
155
|
+
const t = transcript[i];
|
|
156
|
+
if (t && t.role === "assistant" && t.text && t.text.includes("[[SETUP_COMPLETE]]")) {
|
|
157
|
+
finalTurn = t.text;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!finalTurn) return null;
|
|
162
|
+
const fences = [...finalTurn.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
|
|
163
|
+
for (let j = fences.length - 1; j >= 0; j--) {
|
|
164
|
+
try {
|
|
165
|
+
const obj = JSON.parse(fences[j][1].trim());
|
|
166
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj) && obj._kind === "mover-profile") {
|
|
167
|
+
return obj;
|
|
168
|
+
}
|
|
169
|
+
} catch (_) { /* malformed — try an earlier fence in THIS turn only */ }
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Allowlisted profile keys the builders consume. The profile block is
|
|
175
|
+
// LLM-generated (and the conversation may contain imported text), so anything
|
|
176
|
+
// outside this set is dropped, every value is coerced to a length-capped string,
|
|
177
|
+
// and arrays are joined from strings only — a malformed or hostile block cannot
|
|
178
|
+
// inject structure or unbounded content into the Engine.
|
|
179
|
+
const PROFILE_KEYS = [
|
|
180
|
+
"name", "work", "outcome90", "metric", "measurement", "bottleneck",
|
|
181
|
+
"dailyAction", "sacrifice", "antiidentity", "avoid", "drive", "currentSelf",
|
|
182
|
+
"evolvingSelf", "leverage", "runway", "tools", "network", "energyWindows",
|
|
183
|
+
"vitality", "vitalityTarget", "faith", "faithPractice", "purpose", "lens",
|
|
184
|
+
"projects",
|
|
185
|
+
];
|
|
186
|
+
const PROFILE_VALUE_MAX = 600;
|
|
187
|
+
const PROJECT_LIMIT = 20;
|
|
188
|
+
|
|
189
|
+
// STRUCTURE GUARD — sanitize ONE inline value before it's interpolated into an Engine file. Values
|
|
190
|
+
// seed CONTENT in the user's words but must never inject the structure the workflows parse. Shared by
|
|
191
|
+
// the agent profile (cleanProfile) AND the heuristic field map (buildFieldMap), so whichever wins the
|
|
192
|
+
// merge is sanitized exactly once. Defense:
|
|
193
|
+
// 1. collapse newlines -> a "\n\n## Heading" can't open a new block
|
|
194
|
+
// 2. neutralize "<" -> kills "<!-- ..." comments + raw HTML, anywhere
|
|
195
|
+
// 3. escape EVERY pipe -> a value can't add a Markdown table column (Bug3)
|
|
196
|
+
// 4. de-fang a leading marker -> a line-leading value can't open a heading / hr / blockquote /
|
|
197
|
+
// table / list / code fence / ordered list / image ("!" — Bug7)
|
|
198
|
+
function sanitizeValue(s, max = PROFILE_VALUE_MAX) {
|
|
199
|
+
s = String(s == null ? "" : s).trim();
|
|
200
|
+
if (!s) return "";
|
|
201
|
+
s = s.replace(/\s*\n+\s*/g, " ").trim();
|
|
202
|
+
s = s.replace(/</g, "<");
|
|
203
|
+
s = s.replace(/\|/g, "\\|");
|
|
204
|
+
s = s.replace(/^(\d+)([.)])(?=\s)/, "$1\\$2");
|
|
205
|
+
s = s.replace(/^([!#>*+\-~=_`])/, "\\$1");
|
|
206
|
+
return s.length > max ? s.slice(0, max) + "…" : s;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function cleanProfile(profile) {
|
|
210
|
+
const out = {};
|
|
211
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) return out;
|
|
212
|
+
for (const k of PROFILE_KEYS) {
|
|
213
|
+
const v = profile[k];
|
|
214
|
+
if (k === "projects") {
|
|
215
|
+
const projects = cleanProjects(v);
|
|
216
|
+
if (projects.length) out.projects = projects;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
let s = null;
|
|
220
|
+
if (typeof v === "string") s = v;
|
|
221
|
+
else if (Array.isArray(v)) s = v.filter((x) => typeof x === "string" && x.trim()).join("; ");
|
|
222
|
+
else if (typeof v === "number" || typeof v === "boolean") s = String(v); // coerce, don't silently drop (Bug8)
|
|
223
|
+
if (s == null) continue;
|
|
224
|
+
const clean = sanitizeValue(s);
|
|
225
|
+
if (clean) out[k] = clean;
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cleanProjectName(s, fallback = "Project") {
|
|
231
|
+
s = sanitizeValue(s, 90)
|
|
232
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ")
|
|
233
|
+
.replace(/\s+/g, " ")
|
|
234
|
+
.replace(/^\.+|\.+$/g, "")
|
|
235
|
+
.trim();
|
|
236
|
+
if (!s) s = fallback;
|
|
237
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(s)) s = `${s} Project`;
|
|
238
|
+
return s.slice(0, 90).trim() || fallback;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseProjectLine(line) {
|
|
242
|
+
let s = String(line || "")
|
|
243
|
+
.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "")
|
|
244
|
+
.trim();
|
|
245
|
+
if (!s) return null;
|
|
246
|
+
let parts = s.split(/\s*(?::|->| - )\s+/);
|
|
247
|
+
if (parts.length < 2 && s.includes(",")) {
|
|
248
|
+
const i = s.indexOf(",");
|
|
249
|
+
parts = [s.slice(0, i), s.slice(i + 1)];
|
|
250
|
+
}
|
|
251
|
+
const name = cleanProjectName(parts.shift() || "");
|
|
252
|
+
const rest = sanitizeValue(parts.join(": "), 300);
|
|
253
|
+
return { name, outcome: rest, state: "" };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function cleanProjectSources(raw) {
|
|
257
|
+
const out = [];
|
|
258
|
+
const push = (v) => {
|
|
259
|
+
if (v == null) return;
|
|
260
|
+
let s = "";
|
|
261
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
262
|
+
s = String(v);
|
|
263
|
+
} else if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
264
|
+
const label = v.label || v.name || v.source || v.connector || v.tool || "";
|
|
265
|
+
const why = v.why || v.reason || v.data || v.use || "";
|
|
266
|
+
s = [label, why].filter(Boolean).join(": ");
|
|
267
|
+
}
|
|
268
|
+
s = sanitizeValue(s, 220);
|
|
269
|
+
if (s && !out.some((x) => x.toLowerCase() === s.toLowerCase())) out.push(s);
|
|
270
|
+
};
|
|
271
|
+
if (Array.isArray(raw)) raw.forEach(push);
|
|
272
|
+
else if (typeof raw === "string") raw.split(/\n+|;+/).forEach((x) => push(x.trim()));
|
|
273
|
+
else push(raw);
|
|
274
|
+
return out.slice(0, 8);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function cleanProjects(raw) {
|
|
278
|
+
const out = [];
|
|
279
|
+
const push = (p) => {
|
|
280
|
+
if (!p) return;
|
|
281
|
+
if (typeof p === "string") p = parseProjectLine(p);
|
|
282
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) return;
|
|
283
|
+
const name = cleanProjectName(p.name || p.title || p.project || "");
|
|
284
|
+
const outcome = sanitizeValue(p.outcome || p.goal || p.win || p.target || "", 300);
|
|
285
|
+
const state = sanitizeValue(p.state || p.status || p.now || p.currentState || "", 240);
|
|
286
|
+
const priority = sanitizeValue(p.priority || p.rank || p.order || p.importance || "", 80);
|
|
287
|
+
const nextAction = sanitizeValue(p.nextAction || p.next || p.nextStep || p.dailyAction || p.action || "", 260);
|
|
288
|
+
const blocker = sanitizeValue(p.blocker || p.bottleneck || p.risk || p.constraint || "", 260);
|
|
289
|
+
const metric = sanitizeValue(p.metric || p.measure || p.kpi || p.targetMetric || "", 180);
|
|
290
|
+
const cadence = sanitizeValue(p.cadence || p.reviewCadence || p.rhythm || "", 160);
|
|
291
|
+
const dataSources = cleanProjectSources(p.dataSources || p.sources || p.connectors || p.inputs || p.pullDataFrom);
|
|
292
|
+
const notes = sanitizeValue(p.notes || p.context || "", 320);
|
|
293
|
+
if (!name || out.some((x) => x.name.toLowerCase() === name.toLowerCase())) return;
|
|
294
|
+
out.push({ name, outcome, state, priority, nextAction, blocker, metric, cadence, dataSources, notes });
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (Array.isArray(raw)) {
|
|
298
|
+
raw.forEach(push);
|
|
299
|
+
} else if (typeof raw === "string") {
|
|
300
|
+
raw
|
|
301
|
+
.split(/\n+|;+/)
|
|
302
|
+
.map((x) => x.trim())
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.forEach(push);
|
|
305
|
+
}
|
|
306
|
+
return out.slice(0, PROJECT_LIMIT);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function projectsRootFor(targetDir) {
|
|
310
|
+
const resolved = path.resolve(targetDir);
|
|
311
|
+
const base = path.basename(resolved).toLowerCase();
|
|
312
|
+
const parent = path.basename(path.dirname(resolved)).toLowerCase();
|
|
313
|
+
if (base === "engine" && parent === "02_areas") {
|
|
314
|
+
return path.resolve(resolved, "..", "..", "01_Projects");
|
|
315
|
+
}
|
|
316
|
+
return path.resolve(path.dirname(resolved), "01_Projects");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function uniqueProjectPath(root, folderName, used) {
|
|
320
|
+
const base = cleanProjectName(folderName);
|
|
321
|
+
let name = base;
|
|
322
|
+
let n = 2;
|
|
323
|
+
while (used.has(name.toLowerCase()) || fs.existsSync(path.join(root, name, "project_brief.md"))) {
|
|
324
|
+
name = `${base} ${n++}`;
|
|
325
|
+
}
|
|
326
|
+
used.add(name.toLowerCase());
|
|
327
|
+
const filePath = path.join(root, name, "project_brief.md");
|
|
328
|
+
const rel = path.relative(root, path.resolve(filePath));
|
|
329
|
+
if (rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel)) {
|
|
330
|
+
throw new Error(`[engine-writer] SAFETY BLOCK: project path escaped 01_Projects (${filePath}).`);
|
|
331
|
+
}
|
|
332
|
+
return filePath;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Sanitize an imported-note digest before it lands in Active_Context.md § 7.
|
|
336
|
+
// The digest is EXTERNAL content (files imported via /api/session/ingest), so —
|
|
337
|
+
// like the agent's structured profile (cleanProfile) — it must not be able to
|
|
338
|
+
// open a markdown block or inject raw HTML into the user's most-parsed Engine
|
|
339
|
+
// file (the dashboard reads Active_Context section-by-heading; a smuggled "## "
|
|
340
|
+
// would corrupt that read). Unlike cleanProfile it PRESERVES newlines (a digest
|
|
341
|
+
// is multi-line by nature): it escapes "<" globally and de-fangs only the
|
|
342
|
+
// LINE-LEADING structural markers per line.
|
|
343
|
+
function sanitizeDigest(s) {
|
|
344
|
+
if (typeof s !== "string" || !s) return "";
|
|
345
|
+
return s
|
|
346
|
+
.replace(/</g, "<")
|
|
347
|
+
.split("\n")
|
|
348
|
+
.map((line) => line
|
|
349
|
+
.replace(/^(\s*)(\d+)([.)])(?=\s)/, "$1$2\\$3")
|
|
350
|
+
.replace(/^(\s*)([#>|*+\-~=_`])/, "$1\\$2"))
|
|
351
|
+
.join("\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ─── Heuristic field extractors ───────────────────────────────────────────────
|
|
355
|
+
//
|
|
356
|
+
// These look for semantic signal in Q text to classify answers. The heuristics
|
|
357
|
+
// are intentionally BROAD — better to categorize under an extra heading than
|
|
358
|
+
// to lose a user's answer. All unmatched pairs fall through to a catch-all
|
|
359
|
+
// section in each file so nothing is silently dropped.
|
|
360
|
+
|
|
361
|
+
const KEYWORDS = {
|
|
362
|
+
name: /\bname\b|\bcall you\b|\bwhat.*call/i,
|
|
363
|
+
projects: /\bevery active\b.*\bprojects?\b|\bactive projects?\b|\bproject inventory\b|\bworking on right now\b|\bcurrently carrying\b|\blife outcome\b|\bwork or life\b.*\boutcome\b/i,
|
|
364
|
+
work: /\bdo\b.*\bfor work\b|\bwhat.*do\b|\bbuild\b|\bproject\b|\bbusiness\b|\bprofession\b|\bcareer\b/i,
|
|
365
|
+
outcome90: /\b90.day\b|\bnext.*quarter\b|\bwin.*90\b|\b90.*win\b|\bthree.month\b|\bnext.*90\b/i,
|
|
366
|
+
bottleneck: /\bbottleneck\b|\bblocking\b|\bbiggest.*challenge\b|\bstuck\b|\bproblem\b/i,
|
|
367
|
+
avoid: /\bavoid\b|\bprocrastin\b|\bresist\b|\bdon.*t want\b|\bhide from\b/i,
|
|
368
|
+
antiidentity: /\banti.identity\b|\bwho.*not.*become\b|\bworse.*version\b|\bfear.*become\b/i,
|
|
369
|
+
drive: /\bdriv\b|\bmotivat\b|\bwhy.*important\b|\bwhat.*fuel\b|\bbedrock\b/i,
|
|
370
|
+
leverage: /\bskills?\b|\bassets?\b|\baudience\b|\bcapital\b|\bleverage\b|\bsuperpower\b/i,
|
|
371
|
+
vitality: /\bsleep\b|\bhealth\b|\bfitness\b|\bexercise\b|\bdiet\b|\bbody\b|\bvitalit\b/i,
|
|
372
|
+
faith: /\bfaith\b|\bspiritual\b|\bpurpose\b|\bgod\b|\bmeaning\b|\bprayer\b|\breligion\b/i,
|
|
373
|
+
sacrifice: /\bsacrific\b|\bgive up\b|\bnot doing\b|\bban\b|\bstop\b.*\buntil\b/i,
|
|
374
|
+
// The setup form's closing question ("how do you like to work? Mornings or
|
|
375
|
+
// evenings...") feeds the Dossier energy windows; without this tag it fell
|
|
376
|
+
// to "misc" and the Dossier rendered [TBD] right after "Saved how you work".
|
|
377
|
+
energyWindows: /\bhow do you like to work\b|\bmornings or evenings\b|\blong blocks\b|\bshort bursts\b|\bwork rhythm\b|\benergy window\b|\bpeak hours\b/i,
|
|
378
|
+
strategy: /\bhypothesis\b|\btest\b|\bstrategy\b|\bmodel\b|\bapproach\b/i,
|
|
379
|
+
dailyAction: /\bdaily\b|\bevery day\b|\bone thing\b|\bcompound\b|\bhabit\b/i,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function classify(q) {
|
|
383
|
+
const lower = q.toLowerCase();
|
|
384
|
+
const hits = [];
|
|
385
|
+
for (const [tag, re] of Object.entries(KEYWORDS)) {
|
|
386
|
+
if (re.test(lower)) hits.push(tag);
|
|
387
|
+
}
|
|
388
|
+
return hits.length ? hits : ["misc"];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Build a field-lookup map from pairs. Each field -> [first matched answer].
|
|
393
|
+
* Later answers for the same field are kept in pairs for the raw Q&A dump.
|
|
394
|
+
*/
|
|
395
|
+
function buildFieldMap(pairs) {
|
|
396
|
+
const map = {};
|
|
397
|
+
for (const { q, a } of pairs) {
|
|
398
|
+
const tags = classify(q);
|
|
399
|
+
for (const tag of tags) {
|
|
400
|
+
// Sanitize the raw answer here (Bug4): these heuristic field values are interpolated INLINE into
|
|
401
|
+
// Strategy / Active_Context / the daily note, and they WIN the merge when the agent profile is
|
|
402
|
+
// absent/empty — so a "\n## " in an answer must not inject a heading. Same guard as the profile.
|
|
403
|
+
if (!map[tag]) map[tag] = sanitizeValue(a);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return map;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Date helpers ─────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
function todayYMD() {
|
|
412
|
+
const d = new Date();
|
|
413
|
+
const y = d.getFullYear();
|
|
414
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
415
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
416
|
+
return `${y}-${m}-${dd}`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function todayMonthDir() {
|
|
420
|
+
const d = new Date();
|
|
421
|
+
const y = d.getFullYear();
|
|
422
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
423
|
+
return `${y}-${m}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─── File builders ────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Render the Q&A dump block used across multiple files.
|
|
430
|
+
* Preserves every pair verbatim so nothing is lost.
|
|
431
|
+
*/
|
|
432
|
+
function renderQABlock(pairs, heading = "### Interview Transcript") {
|
|
433
|
+
if (!pairs.length) return `${heading}\n\n_No transcript recorded._\n`;
|
|
434
|
+
const lines = [`${heading}\n`];
|
|
435
|
+
pairs.forEach(({ q, a }, i) => {
|
|
436
|
+
// Q&A is raw user/agent text embedded verbatim into Engine files — sanitize it (Bug4) so a "\n## "
|
|
437
|
+
// in an answer can't inject a heading that confuses a section-aware parser. sanitizeDigest keeps
|
|
438
|
+
// newlines (a transcript dump stays readable) but de-fangs each line's leading structural marker.
|
|
439
|
+
lines.push(`**Q${i + 1}:** ${sanitizeDigest(q)}\n`);
|
|
440
|
+
lines.push(`**A${i + 1}:** ${sanitizeDigest(a)}\n`);
|
|
441
|
+
});
|
|
442
|
+
return lines.join("\n");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Identity_Prime.md ─────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
function buildIdentityPrime(pairs, fields, date) {
|
|
448
|
+
const name = fields.name || "You";
|
|
449
|
+
const work = fields.work || pairs[0]?.a || "(describe what you do)";
|
|
450
|
+
const avoid = fields.avoid || "(what you avoid — complete during /walkthrough)";
|
|
451
|
+
const anti = fields.antiidentity|| "(anti-identity — complete during /walkthrough)";
|
|
452
|
+
const drive = fields.drive || "(bedrock drive — complete during /walkthrough)";
|
|
453
|
+
const vitality = fields.vitality || "(vitality state — from setup answers)";
|
|
454
|
+
const faith = fields.faith || "(faith/purpose — optional, from setup answers)";
|
|
455
|
+
const sacrifice= fields.sacrifice || "(named during setup or /walkthrough)";
|
|
456
|
+
// Previously collected by the interview but dropped on the floor — now wired
|
|
457
|
+
// into their proper sections. Empty string => the honest placeholder shows.
|
|
458
|
+
const currentSelf = fields.currentSelf || "";
|
|
459
|
+
const evolvingSelf = fields.evolvingSelf || "";
|
|
460
|
+
const faithPractice = fields.faithPractice || "";
|
|
461
|
+
const purpose = fields.purpose || "";
|
|
462
|
+
|
|
463
|
+
// Work-domain pairs for the Q&A sections
|
|
464
|
+
const psychPairs = pairs.filter(({ q }) => classify(q).some(t => ["avoid","antiidentity","drive","sacrifice"].includes(t)));
|
|
465
|
+
const workPairs = pairs.filter(({ q }) => classify(q).some(t => ["work","bottleneck","leverage","strategy","outcome90","dailyAction"].includes(t)));
|
|
466
|
+
const vitalPairs = pairs.filter(({ q }) => classify(q).some(t => ["vitality","faith"].includes(t)));
|
|
467
|
+
const miscPairs = pairs.filter(({ q }) => classify(q).every(t => !["avoid","antiidentity","drive","sacrifice","work","bottleneck","leverage","strategy","outcome90","dailyAction","vitality","faith","name"].includes(t)));
|
|
468
|
+
|
|
469
|
+
return `# IDENTITY PRIME: ${name}
|
|
470
|
+
|
|
471
|
+
**Version:** 1.0
|
|
472
|
+
**Last Updated:** ${date}
|
|
473
|
+
**Generated By:** Mover OS Onboarding Engine (engine-writer.js v1)
|
|
474
|
+
|
|
475
|
+
> This file was seeded from your setup interview. The Q&A is embedded verbatim
|
|
476
|
+
> under the right headings. Deepen each section via /walkthrough and /review-week.
|
|
477
|
+
> Never delete sections — the system reads them for /morning, /debug-resistance,
|
|
478
|
+
> and /analyse-day.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## 0. THE THREAT (The Anti-Identity)
|
|
483
|
+
> *"Pain is the fuel. Fear is the compass."*
|
|
484
|
+
|
|
485
|
+
**The Mediocre Version of Me (from your words):**
|
|
486
|
+
${anti}
|
|
487
|
+
|
|
488
|
+
**I am running FROM this, not just TO something.**
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 1. HARDWARE SPECS (The Diagnosis)
|
|
493
|
+
|
|
494
|
+
* **What I do:** ${work}
|
|
495
|
+
* **Primary Drive (captured):** ${drive}
|
|
496
|
+
* **What I Avoid:** ${avoid}
|
|
497
|
+
* **Archetype:** _(refine during /walkthrough)_
|
|
498
|
+
* **Fatal Flaw:** _(refine during /walkthrough)_
|
|
499
|
+
* **The Law:** _(your personal mantra — set during /walkthrough)_
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## 2. THE PSYCHOLOGICAL FIREWALL
|
|
504
|
+
* **When I feel like a failure:** Fear is data. It means I am attacking the right target.
|
|
505
|
+
* **When I want to give up:** _(add during /walkthrough)_
|
|
506
|
+
* **When I am scared:** Fear is a compass. It points at the thing that matters most.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 3. THE CORE IDENTITY (Who I Am)
|
|
511
|
+
|
|
512
|
+
* **The Mover:** I am Kinetic Energy. I do not wait for "feeling." I move.
|
|
513
|
+
* **The Professional:** I treat this as a war, not a hobby.
|
|
514
|
+
* **My Role:** ${work}
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## 3.4 CURRENT SELF (Honest Baseline)
|
|
519
|
+
|
|
520
|
+
${currentSelf || "_(your honest current self-portrait — captured during /walkthrough)_"}
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 3.5 EVOLVING SELF
|
|
525
|
+
|
|
526
|
+
**The Statement:** "I am becoming ${evolvingSelf || "_(fill during /walkthrough)_"}."
|
|
527
|
+
|
|
528
|
+
**The Evidence (updated weekly by /review-week):**
|
|
529
|
+
- _(first entry added by /review-week)_
|
|
530
|
+
|
|
531
|
+
**The Gap:**
|
|
532
|
+
- _(identified by /analyse-day over time)_
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## 3.7 PRIMARY RESISTANCE FORMS
|
|
537
|
+
|
|
538
|
+
| # | Form | Your Pattern |
|
|
539
|
+
|---|---|---|
|
|
540
|
+
| 1 | ${avoid ? avoid.slice(0, 80) : "(from your setup answers)"} | _(refine during /walkthrough)_ |
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## 3.8 DRIVE VACUUMS
|
|
545
|
+
|
|
546
|
+
**Primary Drives (captured from interview):**
|
|
547
|
+
- ${drive}
|
|
548
|
+
|
|
549
|
+
**The 5-Why Chain:** _(deepen during /walkthrough)_
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## 3.9 SACRIFICE LEDGER
|
|
554
|
+
|
|
555
|
+
**Committed to Sacrifice:**
|
|
556
|
+
- ${sacrifice}
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## 4. THE CODE (Non-Negotiables)
|
|
561
|
+
1. **The Work is the Weapon:** Resistance is the enemy. Action is the cure.
|
|
562
|
+
2. **Input > Output:** I focus on what I control (effort), not what I don't (results).
|
|
563
|
+
3. **Done Beats Perfect:** Imperfect action beats perfect planning.
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## 7. VITALITY STATE (The Body)
|
|
568
|
+
|
|
569
|
+
* **Current State:** ${vitality}
|
|
570
|
+
* **Training / Fuel / Recovery:** _(complete during /walkthrough)_
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## 8. SPIRITUAL STATE (Faith/Purpose)
|
|
575
|
+
|
|
576
|
+
* **What gives the work meaning:** ${faith}
|
|
577
|
+
* **Daily practice:** ${faithPractice || "_(from setup, or add during /walkthrough)_"}
|
|
578
|
+
* **North Star Purpose (what outlasts you):** ${purpose || "_(from setup, or add during /walkthrough)_"}
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## SETUP INTERVIEW — Work & Identity Q&A
|
|
583
|
+
|
|
584
|
+
${renderQABlock(workPairs.length ? workPairs : pairs.slice(0, 5), "### Work & Strategy Answers")}
|
|
585
|
+
|
|
586
|
+
${renderQABlock(psychPairs, "### Psychology & Drive Answers")}
|
|
587
|
+
|
|
588
|
+
${renderQABlock(vitalPairs, "### Vitality & Faith Answers")}
|
|
589
|
+
|
|
590
|
+
${miscPairs.length ? renderQABlock(miscPairs, "### Other Answers") : ""}
|
|
591
|
+
|
|
592
|
+
<!-- PSYCH-PROFILE: seeded from setup interview — run /walkthrough to lock archetype, flaw, law -->
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ── Strategy.md ───────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
function buildStrategy(pairs, fields, date) {
|
|
599
|
+
const work = fields.work || "(what you're building)";
|
|
600
|
+
const outcome90 = fields.outcome90 || "(your 90-day win — from setup)";
|
|
601
|
+
const bottleneck = fields.bottleneck || "(your stated bottleneck — from setup)";
|
|
602
|
+
const sacrifice = fields.sacrifice || "(what you're not doing)";
|
|
603
|
+
const daily = fields.dailyAction|| "(the one daily action that compounds)";
|
|
604
|
+
const leverage = fields.leverage || "(skills, assets, audience)";
|
|
605
|
+
// The metric number + its measurement shape, and the vitality/faith targets —
|
|
606
|
+
// all previously parsed then dropped. Empty => the honest placeholder shows.
|
|
607
|
+
const metric = fields.metric || "";
|
|
608
|
+
const measurement = fields.measurement || "";
|
|
609
|
+
const vitality = fields.vitality || "";
|
|
610
|
+
const vitalityTarget= fields.vitalityTarget|| "";
|
|
611
|
+
const faith = fields.faith || "";
|
|
612
|
+
const faithPractice = fields.faithPractice || "";
|
|
613
|
+
const purpose = fields.purpose || "";
|
|
614
|
+
|
|
615
|
+
return `# STRATEGY DOCTRINE
|
|
616
|
+
|
|
617
|
+
**Version:** 1.0
|
|
618
|
+
**Last Updated:** ${date}
|
|
619
|
+
**Generated By:** Mover OS Onboarding Engine (engine-writer.js v1)
|
|
620
|
+
|
|
621
|
+
> Strategy is a hypothesis, not a plan. Test it, measure it, iterate or kill it.
|
|
622
|
+
> This file was seeded from your setup interview — content in quotes is verbatim
|
|
623
|
+
> from what you told the system. Refine via /pivot-strategy and /review-week.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## 1. WORK HYPOTHESIS (V1.0)
|
|
628
|
+
|
|
629
|
+
**Context:** Seeded from setup interview on ${date}.
|
|
630
|
+
**Status:** TESTING (Day 1)
|
|
631
|
+
|
|
632
|
+
**What you said you're building:**
|
|
633
|
+
> ${work}
|
|
634
|
+
|
|
635
|
+
**Your 90-day win (your words):**
|
|
636
|
+
> ${outcome90}
|
|
637
|
+
|
|
638
|
+
**The Hypothesis:** _If I focus on [${work}], targeting the outcome [${outcome90}], I will reach the win condition above._
|
|
639
|
+
_(Sharpen the hypothesis equation via /pivot-strategy after your first week.)_
|
|
640
|
+
|
|
641
|
+
**The Bottleneck you named:**
|
|
642
|
+
> ${bottleneck}
|
|
643
|
+
|
|
644
|
+
**Your Leverage (skills / assets / audience):**
|
|
645
|
+
> ${leverage}
|
|
646
|
+
|
|
647
|
+
**Daily Cause:**
|
|
648
|
+
> ${daily}
|
|
649
|
+
|
|
650
|
+
**The Sacrifice (what's explicitly NOT being done):**
|
|
651
|
+
> ${sacrifice}
|
|
652
|
+
|
|
653
|
+
**Primary Metric (the ONE number):** ${metric || "_(set during /walkthrough or /pivot-strategy)_"}
|
|
654
|
+
|
|
655
|
+
**Measurement:** ${measurement || "_(single | chain | scalar | streak, set once your metric is defined)_"}
|
|
656
|
+
|
|
657
|
+
**Floors:** _(add specific floor numbers during /walkthrough)_
|
|
658
|
+
|
|
659
|
+
**Experiment Protocol:**
|
|
660
|
+
- **Sample size:** 30 days of execution before evaluating.
|
|
661
|
+
- **DO NOTHING rule:** No strategy changes while the test runs.
|
|
662
|
+
- **I'll know it's working when:** _(add observable signal during /walkthrough)_
|
|
663
|
+
- **I'll know it's failing when:** _(add fail condition during /walkthrough)_
|
|
664
|
+
|
|
665
|
+
### Resistance Defense
|
|
666
|
+
* **Pattern:** _(detected by /analyse-day over time)_
|
|
667
|
+
* **Rule:** _(set during /walkthrough)_
|
|
668
|
+
* **Named fear:** _(from interview — see Identity_Prime.md § 3.8)_
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## 2. VITALITY HYPOTHESIS (V1.0)
|
|
673
|
+
|
|
674
|
+
**Context:** ${vitality || "_(from setup answers if vitality topics arose — see below)_"}
|
|
675
|
+
**Status:** TESTING (Day 1)
|
|
676
|
+
|
|
677
|
+
**Measurement:** ${vitalityTarget ? "scalar" : "_(scalar, once a health target is set)_"}
|
|
678
|
+
${vitalityTarget ? `**Baseline:** ${vitality || "(current state, from setup)"}\n**Target:** ${vitalityTarget}\n\n` : ""}**Floors:** _(add specific floor numbers during /walkthrough)_
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## 3. MINDSET HYPOTHESIS (V1.0)
|
|
683
|
+
|
|
684
|
+
**Context:** ${faith || purpose || "_(from setup answers on faith/purpose — see Identity_Prime.md § 8)_"}
|
|
685
|
+
**Status:** TESTING (Day 1)
|
|
686
|
+
|
|
687
|
+
**Measurement:** ${faithPractice ? "streak" : "_(streak, once a daily practice is named)_"}
|
|
688
|
+
${purpose ? `**North Star:** ${purpose}\n\n` : ""}**Floors:** _(add during /walkthrough)_
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## SETUP INTERVIEW — Strategy Q&A
|
|
693
|
+
|
|
694
|
+
${renderQABlock(pairs, "### Full Interview (all turns)")}
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## ARCHIVED HYPOTHESES (The Graveyard)
|
|
699
|
+
|
|
700
|
+
> Append-only. Never delete.
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## LAB REPORT (Changelog)
|
|
705
|
+
|
|
706
|
+
* **${date}:** V1.0 created — seeded from setup interview via engine-writer.js.
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ── Active_Context.md ─────────────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
function buildActiveContext(pairs, fields, prophecy, digest, date) {
|
|
713
|
+
const work = fields.work || "(what you're building)";
|
|
714
|
+
const outcome90 = fields.outcome90 || "(your 90-day win)";
|
|
715
|
+
const bottleneck = fields.bottleneck || "(your stated bottleneck)";
|
|
716
|
+
const drive = fields.drive || "(your bedrock drive)";
|
|
717
|
+
const sacrifice = fields.sacrifice || "(what you're not doing)";
|
|
718
|
+
const projects = cleanProjects(fields.projects);
|
|
719
|
+
const projectLines = projects.length
|
|
720
|
+
? projects.map((p) => {
|
|
721
|
+
const bits = [
|
|
722
|
+
p.priority ? `Priority: ${p.priority}` : "",
|
|
723
|
+
p.outcome || "Outcome to clarify in /overview",
|
|
724
|
+
p.nextAction ? `Next: ${p.nextAction}` : "",
|
|
725
|
+
].filter(Boolean).join(" | ");
|
|
726
|
+
return `- [[${p.name}]] - ${bits}`;
|
|
727
|
+
}).join("\n")
|
|
728
|
+
: "_Populated as you set up projects._";
|
|
729
|
+
|
|
730
|
+
return `# ACTIVE CONTEXT STATE (Golden Source)
|
|
731
|
+
|
|
732
|
+
> **SYSTEM INSTRUCTION:** This file is the **Absolute Truth** for the current strategic
|
|
733
|
+
> direction. Read this FIRST. If the user starts a new session, align with this state
|
|
734
|
+
> immediately. Ignore generic "helpful assistant" defaults.
|
|
735
|
+
|
|
736
|
+
**Last Updated:** ${date} (seeded by engine-writer.js v1 from setup interview)
|
|
737
|
+
**log_last_run:** never
|
|
738
|
+
**analyse_day_last_run:** never
|
|
739
|
+
**weekly_review_last_run:** never
|
|
740
|
+
**Current Phase:** Initial Setup Complete — Walkthrough Next
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## 1. THE STRATEGIC VERDICT
|
|
745
|
+
> **WRITE RULE:** Only \`/pivot-strategy\` may modify this section. All other workflows: READ-ONLY.
|
|
746
|
+
|
|
747
|
+
**The Model:** ${work}
|
|
748
|
+
**The Goal:** ${outcome90}
|
|
749
|
+
**The Bottleneck:** ${bottleneck}
|
|
750
|
+
**The Drive (fuel):** ${drive}
|
|
751
|
+
**The Sacrifice:** ${sacrifice}
|
|
752
|
+
**The Pivot Reason:** V1 hypothesis — first test. No prior data.
|
|
753
|
+
**The Asset:** _(map to Mover_Dossier.md via /walkthrough)_
|
|
754
|
+
**The Constraint:** ${bottleneck}
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## 2. THE GRAVEYARD (Do Not Resurrect)
|
|
759
|
+
|
|
760
|
+
_Empty at install. Items added when ideas or strategies are killed via \`/log\` or \`/pivot-strategy\`._
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
## 3. TACTICAL STATE (Execution Mode)
|
|
765
|
+
|
|
766
|
+
> Updated by \`/plan-tomorrow\` and \`/analyse-day\`.
|
|
767
|
+
|
|
768
|
+
**Today's Focus:** ${outcome90}
|
|
769
|
+
**The Hard Thing:** _(set by /plan-tomorrow)_
|
|
770
|
+
**Sacrifice:** ${sacrifice}
|
|
771
|
+
**Floor Reminder:** _(set during /walkthrough)_
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## 3.5 WAITING FOR
|
|
776
|
+
|
|
777
|
+
_Empty._
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## 4. TECHNICAL CONTEXT
|
|
782
|
+
|
|
783
|
+
${projectLines}
|
|
784
|
+
|
|
785
|
+
> Everything you do is a project: a trip, a hire, a habit, a product. Each gets a folder, and its context compounds there over time: notes, decisions, dead ends.
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## 5. IDENTITY SNAPSHOT (cached from Identity_Prime.md)
|
|
790
|
+
|
|
791
|
+
**Drive:** ${drive}
|
|
792
|
+
**What I Avoid:** ${fields.avoid || "(from Identity_Prime.md — see §3.7)"}
|
|
793
|
+
**Core Work:** ${work}
|
|
794
|
+
**The Law:** _(set during /walkthrough)_
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## 5.5 ACTIVE PATTERNS
|
|
799
|
+
|
|
800
|
+
_Empty at install. Populated by \`/analyse-day\` pattern detection over time._
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## 5.6 CURRENT STATE (Quick Reference)
|
|
805
|
+
|
|
806
|
+
**Energy:** Unknown (first session — set by /morning)
|
|
807
|
+
**Mode:** Initial Setup Complete
|
|
808
|
+
**Last Session:** ${date} — Onboarding setup interview completed. Engine seeded.
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## 6. ACTIVE SESSIONS (Rolling Buffer)
|
|
813
|
+
|
|
814
|
+
* **${date} — Setup Interview:** ${prophecy || "Setup interview completed. Engine files written. Run /walkthrough next."}
|
|
815
|
+
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
## 7. USER INJECTED CONTEXT (Brain Dump)
|
|
819
|
+
|
|
820
|
+
${digest ? `_Note digest injected during setup (${digest.length} chars):_\n\n${sanitizeDigest(digest.slice(0, 800))}${digest.length > 800 ? "\n\n_(truncated)_" : ""}` : "_Empty._"}
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## 8. BACKLOG (Sidequest Queue)
|
|
825
|
+
|
|
826
|
+
_Empty._
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## 8.5 COMMITMENTS (Outbound Promises)
|
|
831
|
+
|
|
832
|
+
_Empty._
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## 9. SYSTEM PROTOCOLS (Agent Instructions)
|
|
837
|
+
|
|
838
|
+
_Empty. Add agent-specific protocols here as patterns emerge._
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
## My Customizations (Permanent Log)
|
|
843
|
+
|
|
844
|
+
_Empty._
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
## Engine Health
|
|
849
|
+
|
|
850
|
+
\`\`\`
|
|
851
|
+
Last scanned: ${date}
|
|
852
|
+
strategy_status: FRESH
|
|
853
|
+
goals_status: FRESH
|
|
854
|
+
dossier_status: NOT_SET
|
|
855
|
+
analyse_day_status: NEVER
|
|
856
|
+
weekly_review_status: NEVER
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## Workflow State
|
|
862
|
+
|
|
863
|
+
\`\`\`
|
|
864
|
+
setup_date: ${date}
|
|
865
|
+
review_week_day: Sunday
|
|
866
|
+
weekly_review_last_run: never
|
|
867
|
+
analyse_day_last_run: never
|
|
868
|
+
log_last_run: never
|
|
869
|
+
monthly_review_last_run: never
|
|
870
|
+
friction_escalation: L0
|
|
871
|
+
\`\`\`
|
|
872
|
+
`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function buildProjectBrief(project, date) {
|
|
876
|
+
const name = cleanProjectName(project.name);
|
|
877
|
+
const outcome = project.outcome || "[TBD]";
|
|
878
|
+
const state = project.state || "[TBD]";
|
|
879
|
+
const priority = project.priority || "[TBD]";
|
|
880
|
+
const metric = project.metric || "[TBD]";
|
|
881
|
+
const blocker = project.blocker || "[TBD]";
|
|
882
|
+
const nextAction = project.nextAction || "[TBD]";
|
|
883
|
+
const cadence = project.cadence || "[TBD]";
|
|
884
|
+
const notes = project.notes || "";
|
|
885
|
+
const dataSources = Array.isArray(project.dataSources) && project.dataSources.length
|
|
886
|
+
? project.dataSources.map((s) => `- ${s}`).join("\n")
|
|
887
|
+
: "- [TBD]";
|
|
888
|
+
return `# ${name}
|
|
889
|
+
|
|
890
|
+
**Status:** Active
|
|
891
|
+
**Created:** ${date}
|
|
892
|
+
**Source:** Mover OS onboarding
|
|
893
|
+
**Priority:** ${priority}
|
|
894
|
+
|
|
895
|
+
## Outcome
|
|
896
|
+
|
|
897
|
+
${outcome}
|
|
898
|
+
|
|
899
|
+
## Operating Surface
|
|
900
|
+
|
|
901
|
+
- **Priority:** ${priority}
|
|
902
|
+
- **Metric:** ${metric}
|
|
903
|
+
- **Review cadence:** ${cadence}
|
|
904
|
+
|
|
905
|
+
## Current State
|
|
906
|
+
|
|
907
|
+
${state}
|
|
908
|
+
|
|
909
|
+
## Blocker
|
|
910
|
+
|
|
911
|
+
${blocker}
|
|
912
|
+
|
|
913
|
+
## Next Action
|
|
914
|
+
|
|
915
|
+
${nextAction}
|
|
916
|
+
|
|
917
|
+
## Data & Connectors
|
|
918
|
+
|
|
919
|
+
${dataSources}
|
|
920
|
+
|
|
921
|
+
## Context
|
|
922
|
+
|
|
923
|
+
This folder exists so notes, decisions, dead ends, files, and future sessions compound in one place instead of scattering across chats.
|
|
924
|
+
${notes ? `\n\n${notes}\n` : ""}
|
|
925
|
+
|
|
926
|
+
## Next Step
|
|
927
|
+
|
|
928
|
+
Run /overview from this project folder to sharpen the brief before planning work.
|
|
929
|
+
`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function buildProjectFiles(targetDir, fields, date) {
|
|
933
|
+
const projects = cleanProjects(fields.projects);
|
|
934
|
+
if (!projects.length) return [];
|
|
935
|
+
const root = projectsRootFor(targetDir);
|
|
936
|
+
const used = new Set();
|
|
937
|
+
return projects.map((project) => ({
|
|
938
|
+
path: uniqueProjectPath(root, project.name, used),
|
|
939
|
+
content: buildProjectBrief(project, date),
|
|
940
|
+
}));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ── Goals.md ──────────────────────────────────────────────────────────────────
|
|
944
|
+
|
|
945
|
+
function buildGoals(pairs, fields, date) {
|
|
946
|
+
const outcome90 = fields.outcome90 || "(your 90-day win — from setup interview)";
|
|
947
|
+
const work = fields.work || "(what you're building)";
|
|
948
|
+
const drive = fields.drive || "(your bedrock motivation)";
|
|
949
|
+
const sacrifice = fields.sacrifice || "(what you're giving up)";
|
|
950
|
+
const daily = fields.dailyAction|| "(the one daily action)";
|
|
951
|
+
const vitality = fields.vitality || "(from setup — add specifics during /walkthrough)";
|
|
952
|
+
const faith = fields.faith || "(from setup — optional)";
|
|
953
|
+
const vitalityTarget= fields.vitalityTarget|| "";
|
|
954
|
+
const faithPractice = fields.faithPractice || "";
|
|
955
|
+
const purpose = fields.purpose || "";
|
|
956
|
+
|
|
957
|
+
return `# Goals
|
|
958
|
+
|
|
959
|
+
**Version:** 1.0
|
|
960
|
+
**Last Updated:** ${date}
|
|
961
|
+
**Generated By:** Mover OS Onboarding Engine (engine-writer.js v1)
|
|
962
|
+
|
|
963
|
+
> You don't achieve goals — you achieve identities. The goal manifests as a
|
|
964
|
+
> byproduct of becoming the person who can easily achieve it. Standards matter
|
|
965
|
+
> more than targets. Floors matter more than ceilings. Direction matters more
|
|
966
|
+
> than deadlines.
|
|
967
|
+
>
|
|
968
|
+
> This file was seeded from your setup interview. Content in quotes is verbatim
|
|
969
|
+
> from what you told the system.
|
|
970
|
+
|
|
971
|
+
---
|
|
972
|
+
|
|
973
|
+
## Direction: Work
|
|
974
|
+
|
|
975
|
+
**The person I am becoming:** "I am the sort of person who ships [${work}] and wins the 90-day outcome."
|
|
976
|
+
**Root drive:** ${drive}
|
|
977
|
+
**My floor (minimum I refuse to drop below):** _(add specific floor during /walkthrough)_
|
|
978
|
+
**My ceiling (what I'm reaching for):** ${outcome90}
|
|
979
|
+
**Daily cause:** ${daily}
|
|
980
|
+
**Sacrifice required:** ${sacrifice}
|
|
981
|
+
**I will know I'm on track when:** _(set an observable signal during /walkthrough)_
|
|
982
|
+
|
|
983
|
+
### Timeframes (Direction Markers, Not Deadlines)
|
|
984
|
+
- **90-day direction:** ${outcome90}
|
|
985
|
+
- **1-year direction:** _(extrapolate from 90-day win during /walkthrough)_
|
|
986
|
+
- **10-year direction:** _(set during /walkthrough or /review-week)_
|
|
987
|
+
|
|
988
|
+
---
|
|
989
|
+
|
|
990
|
+
## Direction: Vitality
|
|
991
|
+
|
|
992
|
+
**The person I am becoming:** "I am the sort of person who _(add during /walkthrough)_."
|
|
993
|
+
**Root drive:** ${vitality || "(from setup answers)"}
|
|
994
|
+
**My floor:** _(add specific floor during /walkthrough)_
|
|
995
|
+
**My ceiling:** ${vitalityTarget || "_(add during /walkthrough)_"}
|
|
996
|
+
**Daily cause:** _(set during /walkthrough)_
|
|
997
|
+
**Sacrifice required:** _(set during /walkthrough)_
|
|
998
|
+
**I will know I'm on track when:** _(set during /walkthrough)_
|
|
999
|
+
|
|
1000
|
+
### Timeframes
|
|
1001
|
+
- **90-day direction:** _(add during /walkthrough)_
|
|
1002
|
+
- **1-year direction:** _(add during /walkthrough)_
|
|
1003
|
+
- **10-year direction:** _(add during /walkthrough)_
|
|
1004
|
+
|
|
1005
|
+
---
|
|
1006
|
+
|
|
1007
|
+
## Direction: Faith / Purpose
|
|
1008
|
+
|
|
1009
|
+
**The person I am becoming:** "I am the sort of person who _(add during /walkthrough)_."
|
|
1010
|
+
**Root drive:** ${faith || "(optional — add during /walkthrough if relevant)"}
|
|
1011
|
+
**North Star (what outlasts you):** ${purpose || "_(add during /walkthrough)_"}
|
|
1012
|
+
**My floor:** _(add during /walkthrough)_
|
|
1013
|
+
**My ceiling:** _(add during /walkthrough)_
|
|
1014
|
+
**Daily cause:** ${faithPractice || "_(add during /walkthrough)_"}
|
|
1015
|
+
|
|
1016
|
+
### Timeframes
|
|
1017
|
+
- **90-day direction:** _(add during /walkthrough)_
|
|
1018
|
+
- **1-year direction:** _(add during /walkthrough)_
|
|
1019
|
+
- **10-year direction:** _(add during /walkthrough)_
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
## Current Self vs Evolving Self
|
|
1024
|
+
|
|
1025
|
+
> Updated by /setup (initial) and /review-week (quarterly).
|
|
1026
|
+
|
|
1027
|
+
### Current Self (honest snapshot — from your interview)
|
|
1028
|
+
| Domain | Beliefs | Habits | Traits | Floor Standard |
|
|
1029
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1030
|
+
| Work | ${drive.slice(0, 60)} | ${daily.slice(0, 60)} | _(from interview)_ | _(add during /walkthrough)_ |
|
|
1031
|
+
| Vitality | _(from interview)_ | _(from interview)_ | _(from interview)_ | _(add during /walkthrough)_ |
|
|
1032
|
+
| Faith | _(from interview)_ | _(from interview)_ | _(from interview)_ | _(add during /walkthrough)_ |
|
|
1033
|
+
|
|
1034
|
+
### Evolving Self (designed identity)
|
|
1035
|
+
| Domain | Beliefs | Habits | Traits | New Floor Standard |
|
|
1036
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1037
|
+
| Work | _(set during /walkthrough)_ | _(set during /walkthrough)_ | _(set during /walkthrough)_ | _(set during /walkthrough)_ |
|
|
1038
|
+
| Vitality | _(set during /walkthrough)_ | _(set during /walkthrough)_ | _(set during /walkthrough)_ | _(set during /walkthrough)_ |
|
|
1039
|
+
| Faith | _(set during /walkthrough)_ | _(set during /walkthrough)_ | _(set during /walkthrough)_ | _(set during /walkthrough)_ |
|
|
1040
|
+
|
|
1041
|
+
---
|
|
1042
|
+
|
|
1043
|
+
## Anti-Goals (What I Refuse to Become)
|
|
1044
|
+
|
|
1045
|
+
- ${fields.antiidentity || "(from Identity_Prime.md § 0 — anti-identity captured in setup)"}
|
|
1046
|
+
|
|
1047
|
+
---
|
|
1048
|
+
|
|
1049
|
+
## CHANGELOG
|
|
1050
|
+
|
|
1051
|
+
* **${date}:** V1.0 — seeded from setup interview via engine-writer.js.
|
|
1052
|
+
`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ── First Daily Note ──────────────────────────────────────────────────────────
|
|
1056
|
+
|
|
1057
|
+
function buildDailyNote(pairs, fields, prophecy, date) {
|
|
1058
|
+
const outcome90 = fields.outcome90 || "(your 90-day win)";
|
|
1059
|
+
const work = fields.work || "(what you're building)";
|
|
1060
|
+
const bottleneck = fields.bottleneck || "(your stated bottleneck)";
|
|
1061
|
+
const sacrifice = fields.sacrifice || "(what you're not doing)";
|
|
1062
|
+
const daily = fields.dailyAction|| "(the one daily action)";
|
|
1063
|
+
|
|
1064
|
+
return `# ${date}
|
|
1065
|
+
|
|
1066
|
+
## Battery
|
|
1067
|
+
|
|
1068
|
+
| Metric | Value | Target | Notes |
|
|
1069
|
+
| :--- | :--- | :--- | :--- |
|
|
1070
|
+
| Zone (1-7) | [0] | 3-5 | First day — set baseline in /morning |
|
|
1071
|
+
| Battery State | [Charged/Steady/Depleting/Debt] | Steady | |
|
|
1072
|
+
| Deep Work | [0h] | 4h+ | |
|
|
1073
|
+
| Rest (Integrated) | [0m] | 60m+ | |
|
|
1074
|
+
| Sleep | [0h] | 8h | Quality: [Good/Fair/Poor] |
|
|
1075
|
+
| Calories | [0] | [target] | Set floor in /walkthrough |
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
## Focus
|
|
1082
|
+
|
|
1083
|
+
- **Single Test:** Move toward: ${outcome90}
|
|
1084
|
+
- **The Hard Thing:** _(set by /plan-tomorrow — run it after /walkthrough)_
|
|
1085
|
+
- **Sacrifice:** ${sacrifice}
|
|
1086
|
+
- **Floor reminder:** _(set floor numbers during /walkthrough)_
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
## Journal
|
|
1093
|
+
|
|
1094
|
+
> Day 1 prompt: You just told the system what you're building and why it matters.
|
|
1095
|
+
> What's the ONE thing standing between you and the 90-day win you described?
|
|
1096
|
+
> Write it plainly — not the story about it, just the thing itself.
|
|
1097
|
+
|
|
1098
|
+
<!-- Write freely. -->
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
---
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
## Tasks
|
|
1105
|
+
|
|
1106
|
+
*These are important but secondary. Do not let them kill The Single Test.*
|
|
1107
|
+
|
|
1108
|
+
- [ ] Run /walkthrough (the guided tour — teaches how to drive the system)
|
|
1109
|
+
- [ ] Set your floor numbers in /walkthrough
|
|
1110
|
+
- [ ] Run /plan-tomorrow after /walkthrough
|
|
1111
|
+
- [ ] Set the Single Test for tomorrow
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
---
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
## Schedule
|
|
1118
|
+
|
|
1119
|
+
**Total Available:**
|
|
1120
|
+
**Allocated:**
|
|
1121
|
+
**Buffer:** (minimum 1h integrated rest)
|
|
1122
|
+
|
|
1123
|
+
| Task Block | Est. | Range (Clock) | Actual | Variance |
|
|
1124
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1125
|
+
| /walkthrough | 30m | | | |
|
|
1126
|
+
| /plan-tomorrow | 15m | | | |
|
|
1127
|
+
| | | | | |
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
### Battle Plan
|
|
1131
|
+
|
|
1132
|
+
1. Open the dashboard.
|
|
1133
|
+
2. Run /walkthrough to learn the operating system.
|
|
1134
|
+
3. Run /plan-tomorrow to get tomorrow's battle plan.
|
|
1135
|
+
4. Execute the plan tomorrow.
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
## Log
|
|
1142
|
+
|
|
1143
|
+
*Type messy notes here all day. Log Work AND Rest. Note resistance when it shows up.*
|
|
1144
|
+
|
|
1145
|
+
- Setup interview completed. Engine seeded. ${prophecy ? `"${prophecy}"` : ""}
|
|
1146
|
+
|
|
1147
|
+
**What you said you're building:** ${work}
|
|
1148
|
+
**The bottleneck you named:** ${bottleneck}
|
|
1149
|
+
**The daily action you committed to:** ${daily}
|
|
1150
|
+
|
|
1151
|
+
**Food:**
|
|
1152
|
+
|
|
1153
|
+
**Sleep:** Total: | Quality:
|
|
1154
|
+
|
|
1155
|
+
**Resistance encountered:**
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
---
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
## Session Log
|
|
1162
|
+
|
|
1163
|
+
* [${date}] [engine-writer.js] Setup interview completed. Engine files written. 5 files seeded from ${pairs.length} transcript turns.
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
## Metrics
|
|
1170
|
+
|
|
1171
|
+
| Metric | Value | Notes |
|
|
1172
|
+
| :--- | :--- | :--- |
|
|
1173
|
+
| Tasks Planned | 4 | /walkthrough, /plan-tomorrow, setup tasks |
|
|
1174
|
+
| Tasks Completed | [0] | |
|
|
1175
|
+
| Completion Rate | [0%] | |
|
|
1176
|
+
| Zombie Tasks | 0 | Day 1 — no carry-over |
|
|
1177
|
+
| Deep Work Actual | [0h] | |
|
|
1178
|
+
| Single Test Hit? | [Y/N] | |
|
|
1179
|
+
| Work Score | [0/10] | Set by /analyse-day |
|
|
1180
|
+
| Vitality Score | [0/10] | Set by /analyse-day |
|
|
1181
|
+
| Mindset Score | [0/10] | Set by /analyse-day |
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
---
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
## Rudder
|
|
1188
|
+
|
|
1189
|
+
*Written by \`/analyse-day\`.*
|
|
1190
|
+
|
|
1191
|
+
- **Work Verdict:** _(run /analyse-day to populate)_
|
|
1192
|
+
- **Vitality Verdict:** _(run /analyse-day to populate)_
|
|
1193
|
+
- **Mindset Verdict:** _(run /analyse-day to populate)_
|
|
1194
|
+
- **Battery Status:** Day 1 baseline — unknown
|
|
1195
|
+
- **Resistance Form:** _(detected by /analyse-day)_
|
|
1196
|
+
- **Tomorrow's Fix:** Run /plan-tomorrow after /walkthrough.
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
---
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
## Review
|
|
1203
|
+
|
|
1204
|
+
- **Did I do The Hard Thing first?** _(after /plan-tomorrow tomorrow)_
|
|
1205
|
+
- **Did I honor my sacrifice?** [Yes/No]
|
|
1206
|
+
- **Did I hit my floor?** _(set floors during /walkthrough)_
|
|
1207
|
+
- **Belief shift?** [Any paradigm shift today?]
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
`;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// ── Mover_Dossier.md ──────────────────────────────────────────────────────────
|
|
1215
|
+
|
|
1216
|
+
function buildDossier(pairs, fields, date) {
|
|
1217
|
+
const work = fields.work || "[TBD]";
|
|
1218
|
+
const leverage = fields.leverage || "[TBD — your skills, audience, capital, access]";
|
|
1219
|
+
const runway = fields.runway || "[TBD — months of survival capital]";
|
|
1220
|
+
const energy = fields.energyWindows || fields.peakHours || "[TBD]";
|
|
1221
|
+
const tools = fields.tools || "[TBD]";
|
|
1222
|
+
const network = fields.network || "[TBD]";
|
|
1223
|
+
const lvPairs = pairs.filter(({ q }) => classify(q).some((t) => t === "leverage"));
|
|
1224
|
+
|
|
1225
|
+
return `# MOVER DOSSIER: Asset Inventory
|
|
1226
|
+
|
|
1227
|
+
**Version:** 1.0
|
|
1228
|
+
**Last Updated:** ${date}
|
|
1229
|
+
**Generated By:** Mover OS Onboarding Engine (engine-writer.js v1)
|
|
1230
|
+
|
|
1231
|
+
> Your ammunition. The system reads this BEFORE suggesting strategy, so it never
|
|
1232
|
+
> hands you a cold-start plan when you already hold an asset. Seeded from setup;
|
|
1233
|
+
> deepen via /walkthrough and /review-week. Content in quotes is your own words.
|
|
1234
|
+
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
## Narrative Profile
|
|
1238
|
+
|
|
1239
|
+
${work}
|
|
1240
|
+
|
|
1241
|
+
---
|
|
1242
|
+
|
|
1243
|
+
## Intangible Assets (skills, audience, reputation, access)
|
|
1244
|
+
|
|
1245
|
+
- ${leverage}
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
## Survival Capital
|
|
1250
|
+
|
|
1251
|
+
- **Runway:** ${runway}
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
## Tangible Assets (cash, code, equipment, IP)
|
|
1256
|
+
|
|
1257
|
+
- [TBD — list during /walkthrough]
|
|
1258
|
+
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
## Hidden Assets (unused IP, dormant leverage)
|
|
1262
|
+
|
|
1263
|
+
- [TBD — surfaced during /review-week]
|
|
1264
|
+
|
|
1265
|
+
---
|
|
1266
|
+
|
|
1267
|
+
## Network
|
|
1268
|
+
|
|
1269
|
+
- ${network}
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
## Tools & Stack
|
|
1274
|
+
|
|
1275
|
+
- ${tools}
|
|
1276
|
+
|
|
1277
|
+
---
|
|
1278
|
+
|
|
1279
|
+
## Energy Windows
|
|
1280
|
+
|
|
1281
|
+
- **Peak work hours:** ${energy}
|
|
1282
|
+
|
|
1283
|
+
---
|
|
1284
|
+
|
|
1285
|
+
## Environment Design
|
|
1286
|
+
|
|
1287
|
+
- [TBD — captured during /setup Bank I or /walkthrough]
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
${renderQABlock(lvPairs.length ? lvPairs : [], "### Leverage Answers (from setup)")}
|
|
1292
|
+
`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ── Auto_Learnings.md ─────────────────────────────────────────────────────────
|
|
1296
|
+
|
|
1297
|
+
function buildAutoLearnings(date) {
|
|
1298
|
+
return `# Auto Learnings
|
|
1299
|
+
|
|
1300
|
+
**Last Updated:** ${date}
|
|
1301
|
+
|
|
1302
|
+
> Patterns the system detects about how you actually work, with confidence
|
|
1303
|
+
> scores (1-5). Append-only, never delete. Below confidence 3 the system says
|
|
1304
|
+
> "I don't know" rather than guessing. Patterns decay if not reinforced.
|
|
1305
|
+
|
|
1306
|
+
---
|
|
1307
|
+
|
|
1308
|
+
_No patterns yet. /analyse-day populates this over your first weeks. When you
|
|
1309
|
+
correct the AI ("from now on do X"), the correction lands here too._
|
|
1310
|
+
`;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ── Metrics_Log.md ────────────────────────────────────────────────────────────
|
|
1314
|
+
|
|
1315
|
+
function buildMetricsLog(date) {
|
|
1316
|
+
return `# Metrics Log
|
|
1317
|
+
|
|
1318
|
+
**Last Updated:** ${date}
|
|
1319
|
+
|
|
1320
|
+
> 30-day rolling window of daily performance. Written by /analyse-day, read by
|
|
1321
|
+
> /review-week. One row per day.
|
|
1322
|
+
|
|
1323
|
+
| Date | Planned | Done | Rate | Deep Work | Energy | Single Test | Notes |
|
|
1324
|
+
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
|
1325
|
+
| ${date} | — | — | — | — | — | — | Engine seeded; metrics begin after your first /analyse-day |
|
|
1326
|
+
`;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* writeEngine({ transcript, digest, targetDir, dryRun, force })
|
|
1333
|
+
*
|
|
1334
|
+
* Returns { files: [{path, content}], written: bool }
|
|
1335
|
+
*
|
|
1336
|
+
* Safety:
|
|
1337
|
+
* - Throws if targetDir resolves to or within 02_Areas/Engine/ (unless allowEngineWrite).
|
|
1338
|
+
* - Throws if targetDir already holds a non-empty Identity_Prime.md OR Strategy.md
|
|
1339
|
+
* (the setup-only SENTINEL files) AND force !== true. The structure installer
|
|
1340
|
+
* ships the other Engine files (Active_Context / Goals / Dossier / Auto_Learnings
|
|
1341
|
+
* / Metrics_Log) as non-empty defaults, so they are NOT guarded — a fresh setup
|
|
1342
|
+
* seeds over those placeholders.
|
|
1343
|
+
* - When dryRun=true (DEFAULT): returns file objects, writes nothing.
|
|
1344
|
+
* - When dryRun=false: writes atomically (tmp + rename).
|
|
1345
|
+
*/
|
|
1346
|
+
function writeEngine({ transcript, digest = "", targetDir, dryRun = true, force = false, allowEngineWrite = false } = {}) {
|
|
1347
|
+
if (!targetDir) throw new Error("[engine-writer] targetDir is required");
|
|
1348
|
+
|
|
1349
|
+
// ── Safety gate: refuse writes to the real Engine (unless a fresh-install commit) ──
|
|
1350
|
+
assertSafeTarget(targetDir, allowEngineWrite);
|
|
1351
|
+
|
|
1352
|
+
// ── Parse transcript ───────────────────────────────────────────────────────
|
|
1353
|
+
// The agent's own structured extraction (the ```json block on its final turn)
|
|
1354
|
+
// wins; the keyword heuristic fills any gaps it left. This is what makes the
|
|
1355
|
+
// GUI files deep + accurate instead of regex-classified guesses.
|
|
1356
|
+
const pairs = pairTurns(transcript || []);
|
|
1357
|
+
const fields = { ...buildFieldMap(pairs), ...cleanProfile(parseProfile(transcript || [])) };
|
|
1358
|
+
const prophecy = extractProphecy(transcript || []);
|
|
1359
|
+
const date = todayYMD();
|
|
1360
|
+
const monthDir = todayMonthDir();
|
|
1361
|
+
|
|
1362
|
+
// ── Build file contents (the full Engine, not a 5-file subset) ─────────────
|
|
1363
|
+
const identityContent = buildIdentityPrime(pairs, fields, date);
|
|
1364
|
+
const strategyContent = buildStrategy(pairs, fields, date);
|
|
1365
|
+
const dossierContent = buildDossier(pairs, fields, date);
|
|
1366
|
+
const goalsContent = buildGoals(pairs, fields, date);
|
|
1367
|
+
const activeContextContent = buildActiveContext(pairs, fields, prophecy, digest, date);
|
|
1368
|
+
const autoLearningsContent = buildAutoLearnings(date);
|
|
1369
|
+
const metricsLogContent = buildMetricsLog(date);
|
|
1370
|
+
const dailyContent = buildDailyNote(pairs, fields, prophecy, date);
|
|
1371
|
+
const projectFiles = buildProjectFiles(targetDir, fields, date);
|
|
1372
|
+
|
|
1373
|
+
const dailiesSubdir = path.join("Dailies", monthDir);
|
|
1374
|
+
const dailyFilename = `Daily - ${date}.md`;
|
|
1375
|
+
|
|
1376
|
+
const files = [
|
|
1377
|
+
{ path: path.join(targetDir, "Identity_Prime.md"), content: identityContent },
|
|
1378
|
+
{ path: path.join(targetDir, "Strategy.md"), content: strategyContent },
|
|
1379
|
+
{ path: path.join(targetDir, "Mover_Dossier.md"), content: dossierContent },
|
|
1380
|
+
{ path: path.join(targetDir, "Goals.md"), content: goalsContent },
|
|
1381
|
+
{ path: path.join(targetDir, "Active_Context.md"), content: activeContextContent },
|
|
1382
|
+
{ path: path.join(targetDir, "Auto_Learnings.md"), content: autoLearningsContent },
|
|
1383
|
+
{ path: path.join(targetDir, "Metrics_Log.md"), content: metricsLogContent },
|
|
1384
|
+
{ path: path.join(targetDir, dailiesSubdir, dailyFilename), content: dailyContent },
|
|
1385
|
+
...projectFiles,
|
|
1386
|
+
];
|
|
1387
|
+
|
|
1388
|
+
if (dryRun) {
|
|
1389
|
+
return { files, written: false };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// ── Write guard: refuse only if a REAL prior setup already exists ──────────
|
|
1393
|
+
// Gate on the SENTINEL files — Identity_Prime.md and Strategy.md — because they
|
|
1394
|
+
// are the only targets created exclusively by setup. The structure installer
|
|
1395
|
+
// ships Active_Context / Goals / Mover_Dossier / Auto_Learnings / Metrics_Log as
|
|
1396
|
+
// NON-EMPTY defaults at install (and ships Identity/Strategy only as *_template.md).
|
|
1397
|
+
// So a "refuse if ANY target is non-empty" guard blocked EVERY real install's very
|
|
1398
|
+
// first setup write — the user finished the interview, committed, and silently got
|
|
1399
|
+
// "you already have a record" while nothing landed. If either sentinel holds content
|
|
1400
|
+
// a genuine Engine exists → refuse; otherwise this is a fresh install and setup may
|
|
1401
|
+
// seed over the shipped defaults. The server still serializes commits so two
|
|
1402
|
+
// finalizes can't both pass. (force=true bypasses entirely — a known fresh target.)
|
|
1403
|
+
if (!force) {
|
|
1404
|
+
const SENTINEL_FILES = new Set(["Identity_Prime.md", "Strategy.md"]);
|
|
1405
|
+
for (const { path: filePath } of files) {
|
|
1406
|
+
if (!SENTINEL_FILES.has(path.basename(filePath))) continue;
|
|
1407
|
+
try {
|
|
1408
|
+
const existing = fs.readFileSync(filePath, "utf8");
|
|
1409
|
+
if (existing.trim().length > 0) {
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`[engine-writer] SAFETY BLOCK: ${filePath} already exists and is non-empty. ` +
|
|
1412
|
+
`Refusing to overwrite an existing Engine (force=true is only for a fresh-install target).`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
if (e.code !== "ENOENT") throw e; // ENOENT = fresh; anything else re-throws
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ── Atomic-ish multi-file commit (Bug1+2) ──────────────────────────────────
|
|
1422
|
+
// Write EVERY file to its tmp first; only once all 8 succeed do we rename them in. A failure during
|
|
1423
|
+
// the write phase (disk full, ENOTDIR, perms) unlinks every staged tmp and commits NOTHING — so a
|
|
1424
|
+
// retry is never locked out by a half-seeded sentinel (Identity_Prime/Strategy), and no orphaned
|
|
1425
|
+
// .engine-writer.tmp is left behind. (Per-file rename is itself atomic; once the bytes are on disk a
|
|
1426
|
+
// same-dir rename effectively never fails, so the residual rename-phase window is negligible.)
|
|
1427
|
+
const staged = [];
|
|
1428
|
+
try {
|
|
1429
|
+
for (const { path: filePath, content } of files) {
|
|
1430
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1431
|
+
const tmp = filePath + ".engine-writer.tmp";
|
|
1432
|
+
fs.writeFileSync(tmp, content, "utf8");
|
|
1433
|
+
staged.push({ tmp, filePath });
|
|
1434
|
+
}
|
|
1435
|
+
} catch (e) {
|
|
1436
|
+
for (const { tmp } of staged) { try { fs.unlinkSync(tmp); } catch (_) {} }
|
|
1437
|
+
throw e; // nothing renamed → no partial Engine, no force=false lockout, no dangling tmp
|
|
1438
|
+
}
|
|
1439
|
+
for (const { tmp, filePath } of staged) {
|
|
1440
|
+
fs.renameSync(tmp, filePath); // atomic: readers never see a partial file
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return { files, written: true };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// ─── Self-test (node engine-writer.js) ───────────────────────────────────────
|
|
1447
|
+
|
|
1448
|
+
if (require.main === module) {
|
|
1449
|
+
const assert = require("assert");
|
|
1450
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "engine-writer-test-"));
|
|
1451
|
+
|
|
1452
|
+
// ── Fixture transcript ─────────────────────────────────────────────────────
|
|
1453
|
+
const FIXTURE_TRANSCRIPT = [
|
|
1454
|
+
{ role: "assistant", text: "What are you building right now, and what's the one outcome that would make the next 90 days a win?" },
|
|
1455
|
+
{ role: "user", text: "I'm building an AI-powered productivity OS for founders. The 90-day win is 100 paying customers." },
|
|
1456
|
+
{ role: "assistant", text: "Solo founder, pre-revenue, got it. What's the real bottleneck — the one thing actually blocking 100 customers?" },
|
|
1457
|
+
{ role: "user", text: "Distribution. I'm building in the open but nobody finds it. I don't know how to get my first 50 users." },
|
|
1458
|
+
{ role: "assistant", text: "Distribution gap, not a product gap. What do you avoid doing even though you know it would work?" },
|
|
1459
|
+
{ role: "user", text: "Cold outreach. I know I should DM people but I always find something else to work on instead." },
|
|
1460
|
+
{ role: "assistant", text: "That's a slogan. What actually happens on a Tuesday when you sit down to do outreach?" },
|
|
1461
|
+
{ role: "user", text: "I open Twitter, see something interesting, start a thread about the product instead. Three hours later I've written content no one reads." },
|
|
1462
|
+
{ role: "assistant", text: "Classic Warped Priority — building feels safer than selling. What drives you at the deepest level? Not the answer you'd put on a bio." },
|
|
1463
|
+
{ role: "user", text: "Honestly? Fear of mediocrity. I watched my dad work a job he hated for 30 years. I can't be that." },
|
|
1464
|
+
{ role: "assistant", text: "Fear of the ordinary as fuel. What skills or leverage do you have that most people in your position don't?" },
|
|
1465
|
+
{ role: "user", text: "I can code. I can write. I have 2,000 Twitter followers who are actually founders, not followers." },
|
|
1466
|
+
{ role: "assistant", text: "Code, writing, warm audience. How's your health / sleep / energy holding up?" },
|
|
1467
|
+
{ role: "user", text: "Inconsistent. I sleep 6 hours most nights. Gym twice a week. No real system." },
|
|
1468
|
+
{
|
|
1469
|
+
role: "assistant",
|
|
1470
|
+
text: "[[SETUP_COMPLETE]]\nYour system can now drive your distribution sprint: 50 warm DMs in 30 days using your writing and your existing audience, with every avoidance pattern flagged and named before it eats another Tuesday. Your first daily plan is ready.",
|
|
1471
|
+
},
|
|
1472
|
+
];
|
|
1473
|
+
|
|
1474
|
+
console.log("\n[engine-writer self-test]\n");
|
|
1475
|
+
console.log("Phase 1: dry-run — assert 8 files produced, nothing written to disk\n");
|
|
1476
|
+
|
|
1477
|
+
const dryResult = writeEngine({
|
|
1478
|
+
transcript: FIXTURE_TRANSCRIPT,
|
|
1479
|
+
digest: "Note digest: 3 Apple Notes imported. Key themes: distribution, cold outreach, warped priority.",
|
|
1480
|
+
targetDir: tmpDir,
|
|
1481
|
+
dryRun: true,
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
assert.strictEqual(dryResult.written, false, "dry-run: written must be false");
|
|
1485
|
+
assert.strictEqual(dryResult.files.length, 8, `dry-run: expected 8 files, got ${dryResult.files.length}`);
|
|
1486
|
+
|
|
1487
|
+
const expectedNames = ["Identity_Prime.md", "Strategy.md", "Active_Context.md", "Goals.md"];
|
|
1488
|
+
for (const name of expectedNames) {
|
|
1489
|
+
const found = dryResult.files.some(f => path.basename(f.path) === name);
|
|
1490
|
+
assert.ok(found, `dry-run: missing expected file ${name}`);
|
|
1491
|
+
}
|
|
1492
|
+
const dailyFile = dryResult.files.find(f => path.basename(f.path).startsWith("Daily - "));
|
|
1493
|
+
assert.ok(dailyFile, "dry-run: missing Daily Note");
|
|
1494
|
+
|
|
1495
|
+
// Spot-check: user's 90-day win must appear in Goals.md
|
|
1496
|
+
const goalsFile = dryResult.files.find(f => path.basename(f.path) === "Goals.md");
|
|
1497
|
+
assert.ok(goalsFile.content.includes("100 paying customers"), "Goals.md must include verbatim 90-day win");
|
|
1498
|
+
|
|
1499
|
+
// Spot-check: anti-identity / drive appears in Identity_Prime.md
|
|
1500
|
+
const ipFile = dryResult.files.find(f => path.basename(f.path) === "Identity_Prime.md");
|
|
1501
|
+
assert.ok(ipFile.content.includes("mediocrity"), "Identity_Prime.md must include drive keyword");
|
|
1502
|
+
|
|
1503
|
+
// Spot-check: [[SETUP_COMPLETE]] is NOT rendered as a question
|
|
1504
|
+
const allQA = dryResult.files.map(f => f.content).join("\n");
|
|
1505
|
+
assert.ok(!allQA.includes("[[SETUP_COMPLETE]]"), "[[SETUP_COMPLETE]] must NOT appear in output files");
|
|
1506
|
+
|
|
1507
|
+
// Spot-check: the agent's structured profile block (on the [[SETUP_COMPLETE]]
|
|
1508
|
+
// turn) wins over the heuristic
|
|
1509
|
+
const withProfile = writeEngine({
|
|
1510
|
+
transcript: FIXTURE_TRANSCRIPT.slice(0, -1).concat([
|
|
1511
|
+
{ role: "assistant", text: '[[SETUP_COMPLETE]]\nYour distribution sprint is ready.\n```json\n{"_kind":"mover-profile","name":"Alex","antiidentity":"the comfortable coaster who never ships"}\n```' },
|
|
1512
|
+
]),
|
|
1513
|
+
targetDir: tmpDir,
|
|
1514
|
+
dryRun: true,
|
|
1515
|
+
});
|
|
1516
|
+
const ipWith = withProfile.files.find((f) => path.basename(f.path) === "Identity_Prime.md");
|
|
1517
|
+
assert.ok(ipWith.content.includes("Alex"), "profile name must appear in Identity_Prime.md");
|
|
1518
|
+
assert.ok(ipWith.content.includes("comfortable coaster"), "profile anti-identity must override the heuristic");
|
|
1519
|
+
assert.strictEqual(withProfile.files.length, 8, "profile path also produces 8 files");
|
|
1520
|
+
|
|
1521
|
+
// Regression guard: the 7 fields the interview used to COLLECT-then-DROP must
|
|
1522
|
+
// now each land in their proper structured destination (not just the verbatim
|
|
1523
|
+
// Q&A dump). metric/measurement -> Strategy; current/evolving self -> Identity;
|
|
1524
|
+
// vitalityTarget -> Goals+Strategy; faithPractice/purpose -> Identity (+Goals).
|
|
1525
|
+
const fullProfile = writeEngine({
|
|
1526
|
+
transcript: FIXTURE_TRANSCRIPT.slice(0, -1).concat([
|
|
1527
|
+
{ role: "assistant", text: "[[SETUP_COMPLETE]]\nReady.\n```json\n" + JSON.stringify({
|
|
1528
|
+
_kind: "mover-profile",
|
|
1529
|
+
metric: "$10k MRR by Sep 1",
|
|
1530
|
+
measurement: "chain",
|
|
1531
|
+
currentSelf: "A builder who hides in code and avoids selling",
|
|
1532
|
+
evolvingSelf: "A founder who ships distribution every single day",
|
|
1533
|
+
vitalityTarget: "From 6h to 7.5h sleep average",
|
|
1534
|
+
faithPractice: "Salah on time, Quran ten minutes daily",
|
|
1535
|
+
purpose: "A free tool that keeps helping people after I am gone",
|
|
1536
|
+
}) + "\n```" },
|
|
1537
|
+
]),
|
|
1538
|
+
targetDir: tmpDir,
|
|
1539
|
+
dryRun: true,
|
|
1540
|
+
});
|
|
1541
|
+
const fp = (name) => fullProfile.files.find((f) => path.basename(f.path) === name).content;
|
|
1542
|
+
assert.ok(fp("Strategy.md").includes("$10k MRR by Sep 1"), "metric must land in Strategy.md");
|
|
1543
|
+
assert.ok(/\*\*Measurement:\*\* chain/.test(fp("Strategy.md")), "measurement type must land as a Strategy declaration");
|
|
1544
|
+
assert.ok(fp("Identity_Prime.md").includes("hides in code"), "currentSelf must land in Identity_Prime.md");
|
|
1545
|
+
assert.ok(fp("Identity_Prime.md").includes("ships distribution every single day"), "evolvingSelf must land in Identity_Prime.md");
|
|
1546
|
+
assert.ok(fp("Goals.md").includes("7.5h sleep average") && fp("Strategy.md").includes("7.5h sleep average"), "vitalityTarget must land in Goals + Strategy");
|
|
1547
|
+
assert.ok(fp("Identity_Prime.md").includes("Salah on time"), "faithPractice must land in Identity_Prime.md");
|
|
1548
|
+
assert.ok(fp("Identity_Prime.md").includes("keeps helping people after I am gone"), "purpose must land in Identity_Prime.md");
|
|
1549
|
+
|
|
1550
|
+
// SECURITY: an earlier turn that echoes imported-note content must NOT poison
|
|
1551
|
+
// the profile when the genuine final block is malformed (parser is scoped to
|
|
1552
|
+
// the [[SETUP_COMPLETE]] turn and requires a strict _kind marker).
|
|
1553
|
+
const poison = writeEngine({
|
|
1554
|
+
transcript: [
|
|
1555
|
+
{ role: "assistant", text: 'Echoing your note:\n```json\n{"_kind":"mover-profile","name":"Injected","work":"Poisoned from an imported note"}\n```' },
|
|
1556
|
+
{ role: "user", text: "Actually I build safe onboarding." },
|
|
1557
|
+
{ role: "assistant", text: '[[SETUP_COMPLETE]]\nReady.\n```json\n{"_kind":"mover-profile", BROKEN' },
|
|
1558
|
+
],
|
|
1559
|
+
targetDir: tmpDir,
|
|
1560
|
+
dryRun: true,
|
|
1561
|
+
});
|
|
1562
|
+
// The echoed text may be faithfully recorded in the verbatim Q&A (it was
|
|
1563
|
+
// "said"), but it must NEVER become a STRUCTURED field the system acts on —
|
|
1564
|
+
// that is the actual injection the scoped+strict parser prevents.
|
|
1565
|
+
const ipPoison = poison.files.find((f) => path.basename(f.path) === "Identity_Prime.md").content;
|
|
1566
|
+
assert.ok(!ipPoison.includes("IDENTITY PRIME: Injected"), "injected name must NOT become the Identity title");
|
|
1567
|
+
const stratPoison = poison.files.find((f) => path.basename(f.path) === "Strategy.md").content;
|
|
1568
|
+
assert.ok(!stratPoison.includes("> Poisoned from an imported note"), "injected work must NOT become a structured field");
|
|
1569
|
+
|
|
1570
|
+
// STRUCTURE INJECTION: a profile value carrying block Markdown must NOT become
|
|
1571
|
+
// real Engine structure. cleanProfile collapses newlines + escapes a leading
|
|
1572
|
+
// structural marker, so an injected "## Heading" stays inline text, never a
|
|
1573
|
+
// heading the workflows would parse as a section. (Regression guard for the
|
|
1574
|
+
// Codex finding on the Phase F field-wiring.)
|
|
1575
|
+
const structInject = writeEngine({
|
|
1576
|
+
transcript: FIXTURE_TRANSCRIPT.slice(0, -1).concat([
|
|
1577
|
+
{ role: "assistant", text: "[[SETUP_COMPLETE]]\nReady.\n```json\n" + JSON.stringify({
|
|
1578
|
+
_kind: "mover-profile",
|
|
1579
|
+
vitalityTarget: "7.5h sleep\n\n## INJECTED VITALITY SECTION",
|
|
1580
|
+
purpose: "## INJECTED PURPOSE HEADING",
|
|
1581
|
+
currentSelf: "honest baseline\n\n---\n\n# INJECTED IDENTITY HEADING",
|
|
1582
|
+
evolvingSelf: "<!-- INJECTED COMMENT that would swallow the file",
|
|
1583
|
+
faithPractice: "```js\nINJECTED FENCE",
|
|
1584
|
+
metric: "1. INJECTED ORDERED ITEM",
|
|
1585
|
+
}) + "\n```" },
|
|
1586
|
+
]),
|
|
1587
|
+
targetDir: tmpDir,
|
|
1588
|
+
dryRun: true,
|
|
1589
|
+
});
|
|
1590
|
+
const siStrat = structInject.files.find((f) => path.basename(f.path) === "Strategy.md").content;
|
|
1591
|
+
const siIdent = structInject.files.find((f) => path.basename(f.path) === "Identity_Prime.md").content;
|
|
1592
|
+
const siAll = structInject.files.map((f) => f.content).join("\n");
|
|
1593
|
+
assert.ok(!/^#{1,6}\s+INJECTED/m.test(siStrat), "Strategy.md must not gain an injected heading from a profile value");
|
|
1594
|
+
assert.ok(!/^#{1,6}\s+INJECTED/m.test(siIdent), "Identity_Prime.md must not gain an injected heading from a profile value");
|
|
1595
|
+
assert.ok(!/<!--\s*INJECTED/.test(siAll), "a profile value must not open an HTML comment");
|
|
1596
|
+
assert.ok(!/^```\w* INJECTED/m.test(siAll), "a profile value must not open a code fence");
|
|
1597
|
+
assert.ok(!/^\d+\.\s+INJECTED/m.test(siAll), "a profile value must not open an ordered list");
|
|
1598
|
+
|
|
1599
|
+
console.log(" Phase 1 PASS — 8 files, profile precedence + injection-resistance verified, nothing written\n");
|
|
1600
|
+
|
|
1601
|
+
// Safety gate test: attempt to target 02_Areas/Engine/ must throw
|
|
1602
|
+
console.log("Phase 1b: safety gate — targeting 02_Areas/Engine/ must throw\n");
|
|
1603
|
+
let threw = false;
|
|
1604
|
+
try {
|
|
1605
|
+
writeEngine({
|
|
1606
|
+
transcript: FIXTURE_TRANSCRIPT,
|
|
1607
|
+
targetDir: path.join(os.homedir(), "Documents", "Obsidian", "Architect_OS", "02_Areas", "Engine"),
|
|
1608
|
+
dryRun: false,
|
|
1609
|
+
});
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
threw = true;
|
|
1612
|
+
assert.ok(e.message.includes("SAFETY BLOCK"), `Expected SAFETY BLOCK, got: ${e.message}`);
|
|
1613
|
+
}
|
|
1614
|
+
assert.ok(threw, "Safety gate must throw for 02_Areas/Engine/ target");
|
|
1615
|
+
console.log(" Phase 1b PASS — safety gate throws correctly\n");
|
|
1616
|
+
|
|
1617
|
+
console.log("Phase 2: real write to temp sandbox — verify files land, then clean up\n");
|
|
1618
|
+
const sandboxDir = path.join(tmpDir, "sandbox-engine");
|
|
1619
|
+
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
1620
|
+
|
|
1621
|
+
const writeResult = writeEngine({
|
|
1622
|
+
transcript: FIXTURE_TRANSCRIPT,
|
|
1623
|
+
digest: "Note digest: 3 Apple Notes imported.",
|
|
1624
|
+
targetDir: sandboxDir,
|
|
1625
|
+
dryRun: false,
|
|
1626
|
+
force: false, // fresh dir — no force needed
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
assert.strictEqual(writeResult.written, true, "real write: written must be true");
|
|
1630
|
+
assert.strictEqual(writeResult.files.length, 8, "real write: 8 files must be written");
|
|
1631
|
+
for (const { path: filePath } of writeResult.files) {
|
|
1632
|
+
assert.ok(fs.existsSync(filePath), `real write: file must exist: ${filePath}`);
|
|
1633
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
1634
|
+
assert.ok(content.trim().length > 100, `real write: file must be non-trivial: ${filePath}`);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
console.log(" Phase 2 PASS — 8 files written to sandbox, content verified\n");
|
|
1638
|
+
|
|
1639
|
+
// Force=false guard: second write must throw
|
|
1640
|
+
let threw2 = false;
|
|
1641
|
+
try {
|
|
1642
|
+
writeEngine({ transcript: FIXTURE_TRANSCRIPT, targetDir: sandboxDir, dryRun: false, force: false });
|
|
1643
|
+
} catch (e) {
|
|
1644
|
+
threw2 = true;
|
|
1645
|
+
assert.ok(e.message.includes("SAFETY BLOCK") || e.message.includes("already exists"), `Unexpected error: ${e.message}`);
|
|
1646
|
+
}
|
|
1647
|
+
assert.ok(threw2, "Second write without force=true must throw");
|
|
1648
|
+
console.log(" Phase 2b PASS — force guard throws on non-empty target\n");
|
|
1649
|
+
|
|
1650
|
+
// Phase 2c — the fresh-install regression guard (the funnel-killing bug).
|
|
1651
|
+
// The structure installer ships Active_Context / Goals / Mover_Dossier /
|
|
1652
|
+
// Auto_Learnings / Metrics_Log as NON-EMPTY defaults, but NOT Identity_Prime.md
|
|
1653
|
+
// or Strategy.md. A fresh setup MUST be allowed to seed over those shipped
|
|
1654
|
+
// defaults; only a genuine prior Engine (sentinels present) may block.
|
|
1655
|
+
console.log("Phase 2c: fresh install with shipped defaults present — must still write\n");
|
|
1656
|
+
const freshDir = path.join(tmpDir, "fresh-with-defaults");
|
|
1657
|
+
fs.mkdirSync(freshDir, { recursive: true });
|
|
1658
|
+
for (const name of ["Active_Context.md", "Goals.md", "Mover_Dossier.md", "Auto_Learnings.md", "Metrics_Log.md"]) {
|
|
1659
|
+
fs.writeFileSync(path.join(freshDir, name), `# ${name}\n\n_(shipped structure default — pre-setup placeholder)_\n`, "utf8");
|
|
1660
|
+
}
|
|
1661
|
+
const freshResult = writeEngine({ transcript: FIXTURE_TRANSCRIPT, targetDir: freshDir, dryRun: false, force: false });
|
|
1662
|
+
assert.strictEqual(freshResult.written, true, "fresh install: must write over shipped defaults (no Identity/Strategy yet)");
|
|
1663
|
+
assert.ok(fs.existsSync(path.join(freshDir, "Identity_Prime.md")), "fresh install: Identity_Prime.md must be created");
|
|
1664
|
+
assert.ok(fs.existsSync(path.join(freshDir, "Strategy.md")), "fresh install: Strategy.md must be created");
|
|
1665
|
+
assert.ok(fs.readFileSync(path.join(freshDir, "Active_Context.md"), "utf8").length > 200, "fresh install: the shipped Active_Context default must be replaced with real content");
|
|
1666
|
+
console.log(" Phase 2c PASS — fresh install seeds over shipped defaults\n");
|
|
1667
|
+
|
|
1668
|
+
// Phase 2d — once a real Engine exists (sentinels present), a re-run must block.
|
|
1669
|
+
let threw3 = false;
|
|
1670
|
+
try {
|
|
1671
|
+
writeEngine({ transcript: FIXTURE_TRANSCRIPT, targetDir: freshDir, dryRun: false, force: false });
|
|
1672
|
+
} catch (e) {
|
|
1673
|
+
threw3 = true;
|
|
1674
|
+
assert.ok(e.message.includes("SAFETY BLOCK"), `Phase 2d unexpected error: ${e.message}`);
|
|
1675
|
+
}
|
|
1676
|
+
assert.ok(threw3, "Phase 2d: a re-run over a real Engine (sentinels present) must block");
|
|
1677
|
+
console.log(" Phase 2d PASS — real Engine (sentinels present) still blocks\n");
|
|
1678
|
+
|
|
1679
|
+
// Cleanup
|
|
1680
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1681
|
+
console.log(" Cleanup: temp dir removed.\n");
|
|
1682
|
+
console.log("[engine-writer self-test] ALL PHASES PASS\n");
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
module.exports = { writeEngine };
|