morphisms 0.1.0-beta.3 → 0.1.0-beta.4

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/lib/cli.js CHANGED
@@ -2,9 +2,10 @@ const pkg = require('../package.json');
2
2
  const { initCommand } = require('./init');
3
3
  const { updateCommand } = require('./update');
4
4
  const { workbenchCommand } = require('./workbench');
5
+ const { onboardingCommand } = require('./onboarding');
5
6
 
6
7
  function help() {
7
- console.log(`Morphisms ${pkg.version}\n\nUsage:\n morphisms init\n morphisms workbench [--port N]\n morphisms update\n morphisms --version`);
8
+ console.log(`Morphisms ${pkg.version}\n\nUsage:\n morphisms init\n morphisms workbench [--port N]\n morphisms onboarding [--thread CHANNEL] [--json]\n morphisms update\n morphisms --version`);
8
9
  }
9
10
 
10
11
  async function main(argv) {
@@ -20,6 +21,7 @@ async function main(argv) {
20
21
  if (command === 'init') return initCommand(rest);
21
22
  if (command === 'update') return updateCommand(rest);
22
23
  if (command === 'workbench') return workbenchCommand(rest);
24
+ if (command === 'onboarding') return onboardingCommand(rest);
23
25
  console.error(`unknown morphisms command: ${command}`);
24
26
  help();
25
27
  return 1;
@@ -0,0 +1,113 @@
1
+ const { spawn } = require('node:child_process');
2
+ const { buildWorkbenchArgv, configuredPort } = require('./workbench');
3
+ const {
4
+ DEFAULT_POLL_INTERVAL_MS,
5
+ DEFAULT_POLL_TIMEOUT_MS,
6
+ DEFAULT_PROBE_TIMEOUT_MS,
7
+ occupiedPortReason,
8
+ probeVersion,
9
+ waitForWorkbench,
10
+ watchChildProcess,
11
+ } = require('./workbench-health');
12
+
13
+ function parseOnboardingArgs(argv) {
14
+ let thread;
15
+ let json = false;
16
+ for (let i = 0; i < argv.length; i += 1) {
17
+ const arg = argv[i];
18
+ if (arg === '--json') {
19
+ json = true;
20
+ } else if (arg === '--thread') {
21
+ thread = argv[++i];
22
+ if (!thread) throw new Error('--thread requires a value');
23
+ } else if (arg.startsWith('--thread=')) {
24
+ thread = arg.slice('--thread='.length);
25
+ if (!thread) throw new Error('--thread requires a value');
26
+ } else {
27
+ throw new Error(`unknown onboarding argument: ${arg}`);
28
+ }
29
+ }
30
+ return { thread, json };
31
+ }
32
+
33
+ function buildOnboardingUrl(port, thread) {
34
+ const base = `http://localhost:${port}/#workbench?onboarding=1`;
35
+ if (!thread) return base;
36
+ return `${base}&thread=${encodeURIComponent(thread)}`;
37
+ }
38
+
39
+ function startDetachedWorkbench(repoRoot, port) {
40
+ const workspaceRegistry = process.env.MORPHISMS_WORKBENCH_REGISTRY || undefined;
41
+ const child = spawn('mor-workbench', buildWorkbenchArgv(repoRoot, { port, workspaceRegistry }), {
42
+ detached: true,
43
+ stdio: 'ignore',
44
+ });
45
+ const childState = watchChildProcess(child);
46
+ child.unref();
47
+ return childState;
48
+ }
49
+
50
+ async function onboardingCommand(argv, options = {}) {
51
+ const stdout = options.stdout || process.stdout;
52
+ const stderr = options.stderr || process.stderr;
53
+ let parsed;
54
+ try {
55
+ parsed = parseOnboardingArgs(argv);
56
+ } catch (error) {
57
+ return reportFailure(error.message, argv.includes('--json'), stdout, stderr);
58
+ }
59
+
60
+ const repoRoot = options.cwd || process.cwd();
61
+ const port = configuredPort(repoRoot);
62
+ const initialProbe = await probeVersion(port, { timeoutMs: options.probeTimeoutMs || DEFAULT_PROBE_TIMEOUT_MS });
63
+ let started = false;
64
+ let healthy = initialProbe;
65
+ let spawnState;
66
+
67
+ if (!initialProbe.ok) {
68
+ if (initialProbe.kind === 'occupied') {
69
+ return reportFailure(occupiedPortReason(port, initialProbe.reason), parsed.json, stdout, stderr);
70
+ }
71
+ started = true;
72
+ try {
73
+ const starter = options.startWorkbench || startDetachedWorkbench;
74
+ spawnState = starter(repoRoot, port);
75
+ } catch (error) {
76
+ return reportFailure(`failed to run mor-workbench: ${error.message}`, parsed.json, stdout, stderr);
77
+ }
78
+ healthy = await waitForWorkbench(port, {
79
+ pollTimeoutMs: options.pollTimeoutMs || DEFAULT_POLL_TIMEOUT_MS,
80
+ pollIntervalMs: options.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS,
81
+ probeTimeoutMs: options.probeTimeoutMs || DEFAULT_PROBE_TIMEOUT_MS,
82
+ spawnState,
83
+ });
84
+ }
85
+
86
+ if (!healthy.ok) {
87
+ return reportFailure(healthy.reason || 'workbench is not healthy', parsed.json, stdout, stderr);
88
+ }
89
+
90
+ const url = buildOnboardingUrl(port, parsed.thread);
91
+ if (parsed.json) {
92
+ stdout.write(`${JSON.stringify({ ok: true, port, started, url })}\n`);
93
+ } else {
94
+ stdout.write(`workbench: running (port ${port})\n${url}\n`);
95
+ }
96
+ return 0;
97
+ }
98
+
99
+ function reportFailure(reason, asJson, stdout, stderr) {
100
+ if (asJson) {
101
+ stdout.write(`${JSON.stringify({ ok: false, reason })}\n`);
102
+ } else {
103
+ stderr.write(`morphisms onboarding failed: ${reason}\n`);
104
+ }
105
+ return 1;
106
+ }
107
+
108
+ module.exports = {
109
+ buildOnboardingUrl,
110
+ onboardingCommand,
111
+ parseOnboardingArgs,
112
+ startDetachedWorkbench,
113
+ };
@@ -0,0 +1,126 @@
1
+ const http = require('node:http');
2
+
3
+ const DEFAULT_PROBE_TIMEOUT_MS = 1000;
4
+ const DEFAULT_POLL_TIMEOUT_MS = 30000;
5
+ const DEFAULT_POLL_INTERVAL_MS = 500;
6
+
7
+ function probeVersion(port, options = {}) {
8
+ const timeoutMs = options.timeoutMs || DEFAULT_PROBE_TIMEOUT_MS;
9
+ return new Promise((resolve) => {
10
+ let settled = false;
11
+ function finish(result) {
12
+ if (settled) return;
13
+ settled = true;
14
+ resolve(result);
15
+ }
16
+
17
+ const req = http.get({
18
+ hostname: '127.0.0.1',
19
+ port,
20
+ path: '/version',
21
+ timeout: timeoutMs,
22
+ agent: false,
23
+ }, (res) => {
24
+ res.resume();
25
+ res.on('end', () => {
26
+ if (res.statusCode === 200) {
27
+ finish({ ok: true, kind: 'healthy' });
28
+ } else {
29
+ finish({ ok: false, kind: 'occupied', reason: `GET /version returned ${res.statusCode}` });
30
+ }
31
+ });
32
+ });
33
+
34
+ req.setTimeout(timeoutMs, () => {
35
+ const error = new Error(`GET /version timed out after ${timeoutMs}ms`);
36
+ error.code = 'ETIMEDOUT';
37
+ req.destroy(error);
38
+ });
39
+ req.on('error', (error) => {
40
+ finish({ ok: false, kind: 'unavailable', reason: error.message, code: error.code });
41
+ });
42
+ });
43
+ }
44
+
45
+ function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+
49
+ function occupiedPortReason(port, detail) {
50
+ return `port ${port} is occupied by something that is not the Morphisms Workbench${detail ? ` (${detail})` : ''}`;
51
+ }
52
+
53
+ function watchChildProcess(child) {
54
+ let failureReason;
55
+
56
+ child.once('error', (error) => {
57
+ failureReason = `failed to run mor-workbench: ${error.message}`;
58
+ });
59
+ child.once('exit', (code, signal) => {
60
+ if (!failureReason) failureReason = childExitReason(code, signal);
61
+ });
62
+ child.once('close', (code, signal) => {
63
+ if (!failureReason) failureReason = childExitReason(code, signal);
64
+ });
65
+
66
+ return {
67
+ getFailureReason() {
68
+ return failureReason;
69
+ },
70
+ };
71
+ }
72
+
73
+ function childExitReason(code, signal) {
74
+ if (signal) return `mor-workbench exited before becoming healthy (signal ${signal})`;
75
+ return `mor-workbench exited before becoming healthy (code ${typeof code === 'number' ? code : 'unknown'})`;
76
+ }
77
+
78
+ function spawnFailureReason(spawnState) {
79
+ if (!spawnState) return undefined;
80
+ if (spawnState.getFailureReason) return spawnState.getFailureReason();
81
+ if (spawnState.getError) {
82
+ const error = spawnState.getError();
83
+ if (error) return `failed to run mor-workbench: ${error.message}`;
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ async function waitForWorkbench(port, options = {}) {
89
+ const pollTimeoutMs = options.pollTimeoutMs || DEFAULT_POLL_TIMEOUT_MS;
90
+ const pollIntervalMs = options.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
91
+ const probeTimeoutMs = options.probeTimeoutMs || DEFAULT_PROBE_TIMEOUT_MS;
92
+ const spawnState = options.spawnState;
93
+ const deadline = Date.now() + pollTimeoutMs;
94
+ let lastReason = 'not probed';
95
+
96
+ for (;;) {
97
+ const failureReason = spawnFailureReason(spawnState);
98
+ if (failureReason) return { ok: false, reason: failureReason };
99
+
100
+ const probe = await probeVersion(port, { timeoutMs: probeTimeoutMs });
101
+ if (probe.ok) return { ok: true };
102
+ if (probe.kind === 'occupied') return { ok: false, reason: occupiedPortReason(port, probe.reason) };
103
+ lastReason = probe.reason || lastReason;
104
+
105
+ const remaining = deadline - Date.now();
106
+ if (remaining <= 0) break;
107
+ await sleep(Math.min(pollIntervalMs, remaining));
108
+ }
109
+
110
+ const failureReason = spawnFailureReason(spawnState);
111
+ if (failureReason) return { ok: false, reason: failureReason };
112
+ return {
113
+ ok: false,
114
+ reason: `workbench did not become healthy on port ${port} within ${Math.round(pollTimeoutMs / 1000)}s (${lastReason})`,
115
+ };
116
+ }
117
+
118
+ module.exports = {
119
+ DEFAULT_POLL_INTERVAL_MS,
120
+ DEFAULT_POLL_TIMEOUT_MS,
121
+ DEFAULT_PROBE_TIMEOUT_MS,
122
+ occupiedPortReason,
123
+ probeVersion,
124
+ waitForWorkbench,
125
+ watchChildProcess,
126
+ };
package/lib/workbench.js CHANGED
@@ -1,6 +1,13 @@
1
1
  const { spawn } = require('node:child_process');
2
2
  const path = require('node:path');
3
3
  const { exists } = require('./files');
4
+ const {
5
+ DEFAULT_POLL_INTERVAL_MS,
6
+ DEFAULT_POLL_TIMEOUT_MS,
7
+ DEFAULT_PROBE_TIMEOUT_MS,
8
+ waitForWorkbench,
9
+ watchChildProcess,
10
+ } = require('./workbench-health');
4
11
 
5
12
  const WORKBENCH_LAUNCH = {
6
13
  source: '.morphisms-contract/issues_front_half/contract.mor',
@@ -91,30 +98,97 @@ function buildWorkbenchArgv(repoRoot, options = {}) {
91
98
  ];
92
99
  }
93
100
 
94
- function workbenchCommand(argv) {
101
+ function childExitPromise(child) {
102
+ return new Promise((resolve) => {
103
+ child.once('error', (error) => resolve({ error }));
104
+ child.once('exit', (code, signal) => resolve({ code, signal }));
105
+ });
106
+ }
107
+
108
+ function waitForExitOrKill(child, graceMs) {
109
+ return new Promise((resolve) => {
110
+ let settled = false;
111
+ let timer;
112
+ function finish() {
113
+ if (settled) return;
114
+ settled = true;
115
+ if (timer) clearTimeout(timer);
116
+ resolve();
117
+ }
118
+ child.once('exit', finish);
119
+ child.once('close', finish);
120
+ timer = setTimeout(() => {
121
+ if (settled) return;
122
+ try {
123
+ child.kill('SIGKILL');
124
+ } catch (_) {
125
+ // The child may already be gone; the command is already on a failure path.
126
+ }
127
+ finish();
128
+ }, graceMs);
129
+ });
130
+ }
131
+
132
+ async function terminateChild(child, options = {}) {
133
+ if (!child || typeof child.kill !== 'function') return;
134
+ const graceMs = options.killGraceMs || 250;
135
+ try {
136
+ child.kill('SIGTERM');
137
+ } catch (_) {
138
+ return;
139
+ }
140
+ await waitForExitOrKill(child, graceMs);
141
+ }
142
+
143
+ async function workbenchCommand(argv, options = {}) {
144
+ const stdout = options.stdout || process.stdout;
145
+ const stderr = options.stderr || process.stderr;
146
+ const cwd = options.cwd || process.cwd();
95
147
  let parsed;
96
148
  try {
97
149
  parsed = parseWorkbenchArgs(argv);
98
150
  } catch (error) {
99
- console.error(error.message);
151
+ stderr.write(`${error.message}\n`);
100
152
  return 1;
101
153
  }
102
- const port = parsed.port || configuredPort(process.cwd());
103
- const childArgv = buildWorkbenchArgv(process.cwd(), parsed);
104
- console.log(`Starting Morphisms Workbench at http://127.0.0.1:${port}/#workbench`);
105
- const child = spawn('mor-workbench', childArgv, { stdio: 'inherit' });
106
- child.on('error', (error) => {
107
- console.error(`failed to run mor-workbench: ${error.message}`);
108
- process.exit(1);
109
- });
110
- child.on('exit', (code, signal) => {
111
- if (signal) {
112
- process.kill(process.pid, signal);
113
- return;
114
- }
115
- process.exit(typeof code === 'number' ? code : 1);
154
+ const port = parsed.port || configuredPort(cwd);
155
+ const childArgv = buildWorkbenchArgv(cwd, parsed);
156
+ stdout.write(`starting workbench on port ${port}...\n`);
157
+ const spawnWorkbench = options.spawnWorkbench || ((args, spawnOptions) => spawn('mor-workbench', args, { cwd: spawnOptions.cwd, stdio: 'inherit' }));
158
+ const child = spawnWorkbench(childArgv, { port, cwd });
159
+ const childExit = childExitPromise(child);
160
+ const spawnState = watchChildProcess(child);
161
+ const healthy = await waitForWorkbench(port, {
162
+ pollTimeoutMs: options.pollTimeoutMs || DEFAULT_POLL_TIMEOUT_MS,
163
+ pollIntervalMs: options.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS,
164
+ probeTimeoutMs: options.probeTimeoutMs || DEFAULT_PROBE_TIMEOUT_MS,
165
+ spawnState,
116
166
  });
117
- return undefined;
167
+ if (!healthy.ok) {
168
+ await terminateChild(child, { killGraceMs: options.killGraceMs });
169
+ stderr.write(`morphisms workbench failed: ${healthy.reason || 'workbench is not healthy'}\n`);
170
+ return 1;
171
+ }
172
+
173
+ stdout.write(`Morphisms Workbench: http://127.0.0.1:${port}/#workbench\n`);
174
+ const exit = await childExit;
175
+ if (exit.error) {
176
+ stderr.write(`failed to run mor-workbench: ${exit.error.message}\n`);
177
+ return 1;
178
+ }
179
+ if (exit.signal) {
180
+ if (!options.noSignalForward) process.kill(process.pid, exit.signal);
181
+ return 1;
182
+ }
183
+ return typeof exit.code === 'number' ? exit.code : 1;
118
184
  }
119
185
 
120
- module.exports = { WORKBENCH_LAUNCH, buildWorkbenchArgv, parseWorkbenchArgs, workbenchCommand };
186
+ module.exports = {
187
+ WORKBENCH_LAUNCH,
188
+ buildWorkbenchArgv,
189
+ configuredPort,
190
+ parsePort,
191
+ parseWorkbenchArgs,
192
+ terminateChild,
193
+ workbenchCommand,
194
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morphisms",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Morphisms workflow runtime shim and onboarding wizard",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "node": ">=18"
20
20
  },
21
21
  "optionalDependencies": {
22
- "@morphisms/bin-linux-x64": "0.1.0-beta.3",
23
- "@morphisms/bin-darwin-arm64": "0.1.0-beta.3"
22
+ "@morphisms/bin-linux-x64": "0.1.0-beta.4",
23
+ "@morphisms/bin-darwin-arm64": "0.1.0-beta.4"
24
24
  }
25
25
  }
@@ -10,8 +10,8 @@ You are the orchestrator. Keep this session focused on coordination; delegate on
10
10
  ## Orient first
11
11
 
12
12
  1. Run `mor agent instructions` and read the S1/S2/S3 paths it reports. If you need the fully rendered prompt, run `mor agent instructions preview`.
13
- 2. Open Workbench at `http://localhost:4400/#workbench` unless local init printed a different port. Use Workbench, not legacy graph routes.
14
- 3. If you are inside a MOR Workbench thread capsule, narrate with `mor-thread reply` and propose viewer movement with `mor-thread suggest`; otherwise narrate in the current agent conversation and include the Workbench deep-link.
13
+ 2. Run `morphisms onboarding` (optionally `--thread <channelId>` when you have a thread id) and open only the Workbench URL it prints. Use Workbench, not legacy graph routes.
14
+ 3. If you are inside a MOR Workbench thread capsule, narrate with `mor-thread reply` and propose viewer movement with `mor-thread suggest`; otherwise narrate in the current agent conversation and include the Workbench deep-link from `morphisms onboarding`.
15
15
 
16
16
  ## Frontier
17
17
 
@@ -79,8 +79,24 @@ A refusal is a success signal from the runtime, not a CLI problem. Read the diag
79
79
 
80
80
  ## Concierge trigger
81
81
 
82
- If `.mor/onboarding-complete` is missing, follow the S3 concierge onboarding section before normal orchestration. At the end, write one line to `.mor/onboarding-complete`:
82
+ If `.mor/onboarding-complete` is missing, stop normal orchestration and run the S3
83
+ "Concierge onboarding" beat script instead. It is the human's first-ever contact with
84
+ Morphisms; the S3 section is the authority on wording and pacing. The shape:
85
+
86
+ 1. Step zero: `morphisms onboarding` (health-gated; the only legal source of a Workbench
87
+ URL), then ask for one real bug or annoyance.
88
+ 2. Baton pass: materialize the `onboarding` thread, greet the human inside it via
89
+ `mor-thread reply`, hand over the deep link from `morphisms onboarding --thread
90
+ onboarding`, and continue the entire conversation in that thread.
91
+ 3. Land the four proofs in order — record filed (show it), a deliberate refusal on the
92
+ graph, an investigation citing their code, and one real attention decision — one
93
+ consented step at a time, using only the ten allowed words from S3.
94
+ 4. Close the journey, offer the replay, then write one line to `.mor/onboarding-complete`:
83
95
 
84
96
  ```text
85
97
  <ISO-8601 date> <first issue id>
86
98
  ```
99
+
100
+ During concierge onboarding, the normal spawn/delegation policy is suspended — the
101
+ onboarding agent works in-session; normal spawn/delegation policy resumes after
102
+ `.mor/onboarding-complete` exists.
@@ -10,8 +10,8 @@ You are the orchestrator. Keep this Claude Code session focused on coordination;
10
10
  ## Orient first
11
11
 
12
12
  1. Run `mor agent instructions` and read the S1/S2/S3 paths it reports. If you need the fully rendered prompt, run `mor agent instructions preview`.
13
- 2. Open Workbench at `http://localhost:4400/#workbench` unless local init printed a different port. Use Workbench, not legacy graph routes.
14
- 3. If you are inside a MOR Workbench thread capsule, narrate with `mor-thread reply` and propose viewer movement with `mor-thread suggest`; otherwise narrate in the current Claude conversation and include the Workbench deep-link.
13
+ 2. Run `morphisms onboarding` (optionally `--thread <channelId>` when you have a thread id) and open only the Workbench URL it prints. Use Workbench, not legacy graph routes.
14
+ 3. If you are inside a MOR Workbench thread capsule, narrate with `mor-thread reply` and propose viewer movement with `mor-thread suggest`; otherwise narrate in the current Claude conversation and include the Workbench deep-link from `morphisms onboarding`.
15
15
 
16
16
  ## Frontier
17
17
 
@@ -79,8 +79,24 @@ A refusal is a success signal from the runtime, not a CLI problem. Read the diag
79
79
 
80
80
  ## Concierge trigger
81
81
 
82
- If `.mor/onboarding-complete` is missing, follow the S3 concierge onboarding section before normal orchestration. At the end, write one line to `.mor/onboarding-complete`:
82
+ If `.mor/onboarding-complete` is missing, stop normal orchestration and run the S3
83
+ "Concierge onboarding" beat script instead. It is the human's first-ever contact with
84
+ Morphisms; the S3 section is the authority on wording and pacing. The shape:
85
+
86
+ 1. Step zero: `morphisms onboarding` (health-gated; the only legal source of a Workbench
87
+ URL), then ask for one real bug or annoyance.
88
+ 2. Baton pass: materialize the `onboarding` thread, greet the human inside it via
89
+ `mor-thread reply`, hand over the deep link from `morphisms onboarding --thread
90
+ onboarding`, and continue the entire conversation in that thread.
91
+ 3. Land the four proofs in order — record filed (show it), a deliberate refusal on the
92
+ graph, an investigation citing their code, and one real attention decision — one
93
+ consented step at a time, using only the ten allowed words from S3.
94
+ 4. Close the journey, offer the replay, then write one line to `.mor/onboarding-complete`:
83
95
 
84
96
  ```text
85
97
  <ISO-8601 date> <first issue id>
86
98
  ```
99
+
100
+ During concierge onboarding, the normal spawn/delegation policy is suspended — the
101
+ onboarding agent works in-session; normal spawn/delegation policy resumes after
102
+ `.mor/onboarding-complete` exists.
@@ -68,51 +68,152 @@ Attention:
68
68
 
69
69
  ## Concierge onboarding
70
70
 
71
- If `.mor/onboarding-complete` does **not** exist, you are in concierge mode. Do this before normal orchestration.
71
+ If `.mor/onboarding-complete` does **not** exist, you are in concierge mode. This is the
72
+ human's first-ever contact with Morphisms. Your job is not to demonstrate features; it is
73
+ to land four proofs, in order, about a problem the human actually has:
74
+
75
+ 1. **My words became a record I can inspect.**
76
+ 2. **The rules are real** — you will try to skip ahead and the graph will visibly refuse you.
77
+ 3. **The evidence is real** — a report citing their code, attached to the record.
78
+ 4. **The system formally summons the human** when a decision needs them.
79
+
80
+ Everything below serves those proofs. Target: proof 1 within 3 minutes of starting; done
81
+ in about 7.
82
+
83
+ ### Non-negotiable rules (entire concierge flow)
84
+
85
+ - **Consent cadence.** A "step" is one movement of the record (its evidence plus the edge
86
+ that uses it). Before each step: one plain sentence of what you will do and why, then
87
+ ask. After: one sentence of what changed, plus a way to see it. Never two steps between
88
+ consents.
89
+ - **Vocabulary.** To the human, use only: record, graph, state, journey, evidence, report,
90
+ plan, attention, thread, Workbench. Never say: hydrate, enrich, walk, payload, fibre,
91
+ morphism, distro, release, plateau, witness, capsule, armed, harness, tmux, artifact.
92
+ The commands you run are machinery; the words you say are the product.
93
+ - **URL rule.** Never print or link a Workbench URL unless `morphisms onboarding` has just
94
+ succeeded; quote only the URL it prints.
95
+ - **Failure narration.** If a command fails unexpectedly, say in the thread what you
96
+ tried, that it failed, and what you will do next — in plain words. Never paste raw
97
+ JSON or diagnostics at the human, never go silent, never pretend it worked.
98
+ - **One surface.** After the baton pass (beat 2), the Workbench thread is the only
99
+ conversation surface. The terminal prints nothing further except a hard-failure notice.
100
+ - **Never fabricate the human's answer.** The attention step uses their real reply.
101
+ - **Spawn policy is suspended.** You work in-session; normal spawn/delegation policy
102
+ resumes after `.mor/onboarding-complete` exists.
103
+ - **Go slow.** The human does not know what Morphisms is. Introduce each of the ten
104
+ allowed words only at the moment the thing it names is on their screen. If they ask a
105
+ question, answer it at their level before moving on.
106
+
107
+ ### Beat 1 — step zero and the one question (terminal, under a minute)
108
+
109
+ Silently run step zero first:
72
110
 
73
- ### 1. Ask for one real annoyance
111
+ ```bash
112
+ morphisms onboarding
113
+ ```
114
+
115
+ This proves the Workbench is alive (starting it if needed) and prints the base URL. If it
116
+ fails, tell the human honestly that the visual Workbench could not start (quote its
117
+ one-line reason), offer to continue in the terminal as a degraded walkthrough, and never
118
+ show a URL.
119
+
120
+ Then ask the single opening question:
121
+
122
+ > Let's make this real. Give me one bug, annoyance, or confusing behavior in this repo.
123
+ > If you don't have one ready, I can scan the repo and propose a small first issue.
124
+
125
+ If they ask you to scan, choose a small truthful annoyance grounded in real files.
126
+ Do not file anything yet — filing happens in the thread, where they can watch.
127
+
128
+ ### Beat 2 — the baton pass (terminal → thread)
129
+
130
+ Create the onboarding thread, greet them in it, and hand over one link.
131
+
132
+ ```bash
133
+ # Materialize the thread channel and its working capsule (port from
134
+ # .morphisms-contract/config.json workbench_port):
135
+ curl -s "http://127.0.0.1:<port>/api/agent/thread/v1?channel=onboarding"
136
+ ```
137
+
138
+ Read `working_capsule_path` from the JSON response; call it `$CAPSULE` below.
139
+
140
+ ```bash
141
+ mor-thread --thread "$CAPSULE" reply --message "👋 I'm your Morphisms agent. This thread is where we'll work — reply here and we'll get started."
142
+ morphisms onboarding --thread onboarding
143
+ ```
144
+
145
+ Print in the terminal ONLY this handoff (with the URL the command printed):
146
+
147
+ > Everything happens in the Workbench from here. Click:
148
+ > `<printed URL>`
149
+ > I've already opened our thread there and said hello.
150
+
151
+ Then wait for their reply:
152
+
153
+ ```bash
154
+ mor-thread --thread "$CAPSULE" next --wait
155
+ ```
156
+
157
+ Loop while it returns no message. After roughly three empty waits, print one gentle
158
+ terminal nudge ("Still here — click the link above when you're ready") and keep waiting.
159
+
160
+ Fallback: if `mor-thread` or the capsule fails, post messages over HTTP instead
161
+ (`POST /api/agent/outbox/v1` with `{"channel":"onboarding","message":"..."}`), read
162
+ replies by polling `GET /api/agent/thread/v1?channel=onboarding`, and skip the look-here
163
+ suggestions in later beats — the flow continues without them.
74
164
 
75
- Say:
165
+ ### Beat 3 — roundtrip proof and the plan
76
166
 
77
- > Let's make this real. Give me one bug, annoyance, or confusing behavior in this repo. If you do not have one ready, I can scan the repo and propose one small first issue.
167
+ When their first reply arrives, set the frame in one short message:
78
168
 
79
- If the human gives an issue, file it. If they ask you to scan, inspect files normally and choose a small truthful annoyance grounded in the repo.
169
+ > Great you made it. Here's the plan: I'll file your bug as a **record**, investigate
170
+ > it, and write a plan — and you'll watch every step move across the graph you're looking
171
+ > at. I'll ask before each step. Ready?
80
172
 
81
- Create the first issue with only add-accepted fields:
173
+ ### Beat 4 proof 1: their words become a record
174
+
175
+ On consent, file it (add-accepted fields only):
82
176
 
83
177
  ```json
84
- {"title":"<short title>","kind":"bug","summary":"<human words plus any repo clue>"}
178
+ {"title":"<short title>","kind":"bug","summary":"<their words plus any repo clue>"}
85
179
  ```
86
180
 
87
181
  ```bash
88
182
  mor issue add --payload /tmp/morphisms-first-issue.json
89
183
  ```
90
184
 
91
- Narrate: "I filed the first `issue` record. The graph should now show it at `journey.triage` and `attention.autonomous_entry` in Workbench: http://localhost:4400/#workbench."
92
-
93
- ### 2. Hydrate and explain the packet
185
+ Report and show:
94
186
 
95
- Run:
187
+ > Your bug is now record `<id>`, sitting at the first state of its journey — you can see
188
+ > it on the graph. Don't take my word for it:
96
189
 
97
190
  ```bash
98
- mor issue hydrate <id> journey.triage
191
+ mor-thread --thread "$CAPSULE" suggest open_record --record-id <id> --label "Show me the record"
99
192
  ```
100
193
 
101
- Narrate the first occurrence: "Hydration is the handoff packet: record fields, persona, context, legal edges, and missing witnesses. I follow this packet instead of guessing."
194
+ Tell them they can press `i` on anything they open to see the raw truth underneath.
195
+
196
+ ### Beat 5 — proof 2: the rules are real
102
197
 
103
- ### 3. Show one deliberate refusal
198
+ Announce it first eyes on the graph before you act:
104
199
 
105
- Before attaching `triage_decision`, intentionally attempt the standard lane once:
200
+ > Before I investigate, let me show you why this isn't just a chat log. I'm going to try
201
+ > to skip straight to investigating **without any evidence** — watch the edge leading out
202
+ > of the first state.
106
203
 
107
204
  ```bash
108
205
  mor issue walk <id> journey.choose_standard_investigation
109
206
  ```
110
207
 
111
- Expect a blocked/refused result such as a missing `triage_decision` witness. Narrate: "This refusal is the safety rail. MOR will not let me move the graph until the typed evidence exists."
208
+ This is refused (the record has no triage evidence yet) and the edge flashes red on the
209
+ graph. Explain:
112
210
 
113
- ### 4. Triage into a real investigation lane
211
+ > The graph refused me. I cannot move your issue until the evidence it requires actually
212
+ > exists. The rules live in the graph — not in my good intentions.
114
213
 
115
- Attach a standard triage decision unless the issue truly needs rigorous review:
214
+ ### Beat 6 proof 3: real evidence from their repo
215
+
216
+ Step 6a (consent, then triage): attach the decision and move — note the contrast:
116
217
 
117
218
  ```json
118
219
  {"decision":"standard","rationale":"Needs one file/line investigation before scoping.","signals":["<signal>"]}
@@ -124,11 +225,10 @@ mor issue walk <id> journey.choose_standard_investigation
124
225
  mor issue hydrate <id> journey.investigating
125
226
  ```
126
227
 
127
- Narrate the graph movement in Workbench.
128
-
129
- ### 5. Investigate with citations
228
+ > This time the same edge turned green — the evidence existed, so the move was legal.
130
229
 
131
- Do a real repo read. The `investigation_report.citations` array must be non-empty and cite real file lines:
230
+ Step 6b (consent, then investigate): do a real repo read. `citations` must be non-empty
231
+ with real `{file,line,excerpt,why_it_matters}` entries:
132
232
 
133
233
  ```json
134
234
  {
@@ -146,9 +246,24 @@ mor issue walk <id> journey.submit_standard_investigation
146
246
  mor issue hydrate <id> journey.scoping
147
247
  ```
148
248
 
149
- ### 6. Scope to a typed front-half outcome
249
+ Report with a 2–3 sentence summary of what you found in their code, then show:
250
+
251
+ ```bash
252
+ mor-thread --thread "$CAPSULE" suggest open_record --record-id <id> --label "Read the investigation report"
253
+ ```
254
+
255
+ > The report is attached to the record itself — it doesn't scroll away.
256
+
257
+ Offer depth exactly once, and don't push it:
258
+
259
+ ```bash
260
+ mor-thread --thread "$CAPSULE" suggest open_context_packet --store issues --dimension journey --state investigating --label "See my briefing for that state"
261
+ ```
262
+
263
+ > Curious what I was told to do there? That briefing — who I'm supposed to be and what I
264
+ > get to see — is defined per state, for every state on this graph.
150
265
 
151
- Attach a scoped plan with a handoff prompt:
266
+ Step 6c (consent, then scope): attach the plan with a handoff prompt:
152
267
 
153
268
  ```json
154
269
  {
@@ -165,37 +280,32 @@ Attach a scoped plan with a handoff prompt:
165
280
 
166
281
  ```bash
167
282
  mor issue enrich <id> scoped_plan --role scoped_plan --payload /tmp/scoped-plan.json
168
- mor issue inspect <id>
169
283
  ```
170
284
 
171
- If the honest outcome is blocked, set `plan_outcome=blocked` and include `blocker`; do not close the journey until after the human-collab step below.
285
+ If the honest outcome is blocked, use `plan_outcome=blocked` with `blocker` and say so
286
+ plainly; do not close the journey until after beat 7.
172
287
 
173
- ### 7. Route one real human-collab moment
288
+ ### Beat 7 proof 4: the system summons them
174
289
 
175
- After the scoped plan is attached and before closing the journey, route the record to the human in the `attention` dimension. First inspect the current attention state:
290
+ Check the current attention state with `mor issue inspect <id>`, then route:
176
291
 
177
292
  ```bash
178
- mor issue inspect <id>
293
+ mor issue walk <id> attention.request_operator_from_entry # if attention=autonomous_entry
294
+ mor issue walk <id> attention.request_operator # if attention=autonomous
179
295
  ```
180
296
 
181
- If `attention=autonomous_entry`, run:
297
+ Then hand them the moment:
182
298
 
183
- ```bash
184
- mor issue walk <id> attention.request_operator_from_entry
185
- ```
299
+ > Now the record is waiting for **you** — you can see it sitting at the waiting-for-you
300
+ > state on the attention graph. This is the other half of Morphisms: agents run on their
301
+ > own, and the graph decides when a human is required. No pings, no hoping someone reads
302
+ > the chat. So, your first real decision: should I close this out with the plan as
303
+ > written, or revise the scope?
186
304
 
187
- If `attention=autonomous`, run:
188
-
189
- ```bash
190
- mor issue walk <id> attention.request_operator
191
- ```
192
-
193
- Tell the human: "The record is now waiting for YOU at `attention.awaiting_operator`. In an armed Workbench thread, this is the `Review now` moment; otherwise use the record card in Workbench: http://localhost:4400/#workbench. Please answer whether I should resume and close this scoped plan, or revise scope."
194
-
195
- Use the human's actual answer as the `operator_decision`; do not fabricate it. For a resume answer, attach:
305
+ Use their actual answer:
196
306
 
197
307
  ```json
198
- {"decision":"resume","rationale":"<human's answer>","instructions":"Proceed with the scoped plan outcome.","answered_questions":["Resume and close this scoped plan?"]}
308
+ {"decision":"resume","rationale":"<their answer>","instructions":"Proceed with the scoped plan outcome.","answered_questions":["Resume and close this scoped plan?"]}
199
309
  ```
200
310
 
201
311
  ```bash
@@ -203,30 +313,33 @@ mor issue enrich <id> operator_decision --role operator_decision --payload /tmp/
203
313
  mor issue walk <id> attention.return_to_autonomous
204
314
  ```
205
315
 
206
- Narrate: "This is how future work pauses only when it needs you, then resumes after your typed decision."
316
+ > That's how future work pauses only when it needs you and resumes on your decision.
207
317
 
208
- ### 8. Close the journey
318
+ ### Beat 8 the whole story, then normal mode
209
319
 
210
- For a ready quick/standard scoped plan, close with:
320
+ Close the journey (consent first):
211
321
 
212
322
  ```bash
213
- mor issue walk <id> journey.complete_scoped_plan
214
- mor issue inspect <id>
323
+ mor issue walk <id> journey.complete_scoped_plan # ready quick/standard plan
324
+ mor issue walk <id> journey.close_blocked_from_scoping # honest blocked outcome
215
325
  ```
216
326
 
217
- For a blocked scoped plan, close with:
327
+ Finale give them the record's full story:
218
328
 
219
329
  ```bash
220
- mor issue walk <id> journey.close_blocked_from_scoping
221
- mor issue inspect <id>
330
+ mor-thread --thread "$CAPSULE" suggest start_replay --record-id <id> --label "Replay the whole story"
222
331
  ```
223
332
 
224
- ### 9. Complete onboarding
333
+ > That's the entire journey of your issue, step by step — arrow keys to move, `i` to
334
+ > inspect anything.
225
335
 
226
- When the first record reaches `journey.done`, write the sentinel:
336
+ Write the sentinel:
227
337
 
228
338
  ```bash
229
339
  printf '%s %s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "<id>" > .mor/onboarding-complete
230
340
  ```
231
341
 
232
- Tell the human: "Onboarding is complete. Normal Morphisms mode is now on; future records will start from the frontier loop."
342
+ Sign off with the one next action:
343
+
344
+ > Onboarding is done. From now on, just tell me about bugs and ideas — they'll appear
345
+ > here as records and move across this graph. Keep this tab: it's your workflow's home.
@@ -5,7 +5,7 @@ This is the S2 workspace layer for a repo initialized by `morphisms init`. It ap
5
5
  ## Startup
6
6
 
7
7
  - Run `mor agent instructions` before orchestrating. Read the reported S1/S2/S3 files; use `mor agent instructions preview` only when you need the rendered stack.
8
- - Workbench is the lens: `http://localhost:4400/#workbench` unless `.morphisms-contract/config.json` says a different `workbench_port`.
8
+ - Workbench is the lens: run `morphisms onboarding` first and use only the Workbench URL it prints.
9
9
  - Keep runtime state under `.mor/`. Do not edit SQLite files or registry material by hand.
10
10
  - Do not create `.mor/onboarding-complete`; the onboarding agent writes it only after the first real issue reaches `journey.done`.
11
11