@foldspace_npm/harness 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -138,6 +138,11 @@ The generated npm scripts intentionally remain the normal project interface;
138
138
  has an SDK without the configured agent.
139
139
 
140
140
  Use `foldspace help attach` for mode requirements, effects, and safety options.
141
+ Coding agents should run `foldspace attach --daemon` so the invoking tool
142
+ returns after `[lifecycle] inspect_registration:…`. Foreground attach remains
143
+ the default for humans watching the terminal. `attach --status` and
144
+ `attach --stop` inspect and tear down a daemon. Observe customer-app network
145
+ traffic after `inject` and before attach; attach owns the debug port.
141
146
 
142
147
  ### Verify actions through the agent
143
148
 
@@ -172,6 +177,7 @@ npm i -D @foldspace_npm/harness
172
177
  "build": "foldspace build",
173
178
  "inject": "foldspace inject",
174
179
  "attach": "foldspace attach",
180
+ "attach:daemon": "foldspace attach --daemon",
175
181
  "deploy": "foldspace deploy"
176
182
  } }
177
183
  ```
package/bin/attach.mjs CHANGED
@@ -1012,7 +1012,7 @@ async function cleanup() {
1012
1012
  return result;
1013
1013
  }
1014
1014
 
1015
- process.on("SIGINT", () => {
1015
+ function requestDetach() {
1016
1016
  if (shuttingDown) return;
1017
1017
  shuttingDown = true;
1018
1018
  void cleanup().finally(() => {
@@ -1020,7 +1020,10 @@ process.on("SIGINT", () => {
1020
1020
  console.log(`\nDetached. Served the local bundle ${served} time(s).`);
1021
1021
  process.exit(0);
1022
1022
  });
1023
- });
1023
+ }
1024
+
1025
+ process.on("SIGINT", requestDetach);
1026
+ process.on("SIGTERM", requestDetach);
1024
1027
 
1025
1028
  // A closed socket does not mean the browser closed. Chrome drops the CDP
1026
1029
  // connection on target churn and transient errors too, and exiting on those
package/bin/cli.mjs CHANGED
@@ -5,6 +5,10 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { runInit } from "../src/init.mjs";
8
+ import {
9
+ classifyAttachControl,
10
+ runAttachControl,
11
+ } from "../src/attach-daemon.mjs";
8
12
  import {
9
13
  CLI_COMMANDS,
10
14
  createCliRegistry,
@@ -151,10 +155,32 @@ if (!command || command === "--help" || command === "-h") {
151
155
  } else {
152
156
  try {
153
157
  const definition = commandByName(command);
154
- runScript(
155
- definition.entry,
156
- normalizeCommandArgs(definition, args),
157
- );
158
+ const normalized = normalizeCommandArgs(definition, args);
159
+ if (command === "attach") {
160
+ const control = classifyAttachControl(normalized);
161
+ if (control) {
162
+ Promise.resolve()
163
+ .then(() =>
164
+ runAttachControl(control, normalized, {
165
+ attachScript: path.join(here, definition.entry),
166
+ spawnFn: spawn,
167
+ }),
168
+ )
169
+ .then((report) => {
170
+ console.log(JSON.stringify(report, null, 2));
171
+ if (control === "daemon" && report.ok === false) {
172
+ process.exitCode = 1;
173
+ }
174
+ })
175
+ .catch((error) => {
176
+ fail(error instanceof Error ? error.message : String(error));
177
+ });
178
+ } else {
179
+ runScript(definition.entry, normalized);
180
+ }
181
+ } else {
182
+ runScript(definition.entry, normalized);
183
+ }
158
184
  } catch (error) {
159
185
  fail(error instanceof Error ? error.message : String(error));
160
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldspace_npm/harness",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Build and verify portable Foldspace action artifacts against a live app.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,268 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const ATTACH_DAEMON_ENV = "FOLDSPACE_ATTACH_DAEMON";
5
+ export const ATTACH_DAEMON_STATE_FILE = "attach-daemon.json";
6
+ export const ATTACH_DAEMON_LOG_FILE = "attach-daemon.log";
7
+
8
+ const CONTROL_FLAGS = ["--daemon", "--status", "--stop"];
9
+ const LIFECYCLE_PATTERN =
10
+ /\[lifecycle\] inspect_registration:(registration_ok|registration_mismatch)/g;
11
+
12
+ export function projectRoot(env = process.env, cwd = process.cwd()) {
13
+ return env.FOLDSPACE_PROJECT_DIR || cwd;
14
+ }
15
+
16
+ export function daemonPaths(root) {
17
+ const dir = path.join(root, ".foldspace-dev");
18
+ return {
19
+ dir,
20
+ stateFile: path.join(dir, ATTACH_DAEMON_STATE_FILE),
21
+ logFile: path.join(dir, ATTACH_DAEMON_LOG_FILE),
22
+ };
23
+ }
24
+
25
+ export function isProcessAlive(pid, killFn = process.kill) {
26
+ if (!Number.isInteger(pid) || pid <= 0) return false;
27
+ try {
28
+ killFn(pid, 0);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ export function classifyAttachControl(args) {
36
+ const selected = CONTROL_FLAGS.filter((flag) => args.includes(flag));
37
+ if (selected.length > 1) {
38
+ throw new Error("--daemon, --status, and --stop cannot be used together");
39
+ }
40
+ if (selected[0] === "--daemon") return "daemon";
41
+ if (selected[0] === "--status") return "status";
42
+ if (selected[0] === "--stop") return "stop";
43
+ return null;
44
+ }
45
+
46
+ export function childAttachArgs(args) {
47
+ return args.filter((token) => !CONTROL_FLAGS.includes(token));
48
+ }
49
+
50
+ export function parseRegistrationLifecycle(logText) {
51
+ let last = null;
52
+ const matches = String(logText ?? "").matchAll(LIFECYCLE_PATTERN);
53
+ for (const match of matches) {
54
+ last = {
55
+ state: match[1],
56
+ ok: match[1] === "registration_ok",
57
+ };
58
+ }
59
+ return last;
60
+ }
61
+
62
+ export function readDaemonState(root) {
63
+ const { stateFile } = daemonPaths(root);
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
66
+ if (!parsed || typeof parsed !== "object") return null;
67
+ return parsed;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export function writeDaemonState(root, state) {
74
+ const { dir, stateFile } = daemonPaths(root);
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`);
77
+ }
78
+
79
+ export function clearDaemonState(root) {
80
+ const { stateFile } = daemonPaths(root);
81
+ try {
82
+ fs.unlinkSync(stateFile);
83
+ } catch (error) {
84
+ if (error?.code !== "ENOENT") throw error;
85
+ }
86
+ }
87
+
88
+ function sleep(ms) {
89
+ return new Promise((resolve) => setTimeout(resolve, ms));
90
+ }
91
+
92
+ export async function waitForRegistration({
93
+ logFile,
94
+ pid,
95
+ attempts = 60,
96
+ intervalMs = 250,
97
+ readFile = fs.readFileSync,
98
+ isAlive = isProcessAlive,
99
+ delay = sleep,
100
+ } = {}) {
101
+ for (let attempt = 0; attempt < attempts; attempt++) {
102
+ let text = "";
103
+ try {
104
+ text = readFile(logFile, "utf8");
105
+ } catch (error) {
106
+ if (error?.code !== "ENOENT") throw error;
107
+ }
108
+ const registration = parseRegistrationLifecycle(text);
109
+ if (registration) return registration;
110
+ if (!isAlive(pid)) {
111
+ throw new Error(
112
+ `Attach daemon exited before inspect_registration. Check ${logFile}`,
113
+ );
114
+ }
115
+ await delay(intervalMs);
116
+ }
117
+ throw new Error(
118
+ `Attach daemon did not report inspect_registration. Check ${logFile}`,
119
+ );
120
+ }
121
+
122
+ export function liveDaemon(root, isAlive = isProcessAlive) {
123
+ const state = readDaemonState(root);
124
+ if (!state?.pid || !isAlive(state.pid)) return null;
125
+ return state;
126
+ }
127
+
128
+ function daemonReport(state, extra = {}) {
129
+ let logText = "";
130
+ if (state?.logPath) {
131
+ try {
132
+ logText = fs.readFileSync(state.logPath, "utf8");
133
+ } catch {
134
+ logText = "";
135
+ }
136
+ }
137
+ const registration = parseRegistrationLifecycle(logText);
138
+ return {
139
+ daemon: true,
140
+ pid: state?.pid ?? null,
141
+ logPath: state?.logPath ?? null,
142
+ ...(registration
143
+ ? { ok: registration.ok, registration: registration.state }
144
+ : {}),
145
+ ...extra,
146
+ };
147
+ }
148
+
149
+ export async function stopAttachDaemon({
150
+ root,
151
+ killFn = process.kill,
152
+ isAlive = isProcessAlive,
153
+ delay = sleep,
154
+ attempts = 20,
155
+ intervalMs = 100,
156
+ } = {}) {
157
+ const state = readDaemonState(root);
158
+ if (!state?.pid || !isAlive(state.pid)) {
159
+ clearDaemonState(root);
160
+ return { daemon: true, stopped: true, pid: state?.pid ?? null, reused: false };
161
+ }
162
+
163
+ try {
164
+ killFn(state.pid, "SIGTERM");
165
+ } catch (error) {
166
+ if (error?.code !== "ESRCH") throw error;
167
+ }
168
+
169
+ for (let attempt = 0; attempt < attempts; attempt++) {
170
+ if (!isAlive(state.pid)) break;
171
+ await delay(intervalMs);
172
+ }
173
+ if (isAlive(state.pid)) {
174
+ try {
175
+ killFn(state.pid, "SIGKILL");
176
+ } catch (error) {
177
+ if (error?.code !== "ESRCH") throw error;
178
+ }
179
+ }
180
+
181
+ clearDaemonState(root);
182
+ return { daemon: true, stopped: true, pid: state.pid };
183
+ }
184
+
185
+ export async function startAttachDaemon({
186
+ root,
187
+ attachScript,
188
+ args,
189
+ spawnFn,
190
+ env = process.env,
191
+ wait = waitForRegistration,
192
+ isAlive = isProcessAlive,
193
+ killFn = process.kill,
194
+ } = {}) {
195
+ const existing = liveDaemon(root, isAlive);
196
+ if (existing) {
197
+ return daemonReport(existing, { reused: true, alive: true });
198
+ }
199
+
200
+ const stale = readDaemonState(root);
201
+ if (stale) clearDaemonState(root);
202
+
203
+ const paths = daemonPaths(root);
204
+ fs.mkdirSync(paths.dir, { recursive: true });
205
+ fs.writeFileSync(paths.logFile, "");
206
+ const logFd = fs.openSync(paths.logFile, "a");
207
+ let child;
208
+ try {
209
+ child = spawnFn(process.execPath, [attachScript, ...childAttachArgs(args)], {
210
+ detached: true,
211
+ env: { ...env, [ATTACH_DAEMON_ENV]: "1" },
212
+ stdio: ["ignore", logFd, logFd],
213
+ });
214
+ } finally {
215
+ fs.closeSync(logFd);
216
+ }
217
+ if (typeof child.unref === "function") child.unref();
218
+
219
+ const state = {
220
+ pid: child.pid,
221
+ startedAt: new Date().toISOString(),
222
+ logPath: paths.logFile,
223
+ };
224
+ writeDaemonState(root, state);
225
+
226
+ try {
227
+ const registration = await wait({
228
+ logFile: paths.logFile,
229
+ pid: child.pid,
230
+ isAlive,
231
+ });
232
+ return daemonReport(state, {
233
+ reused: false,
234
+ ready: true,
235
+ ok: registration.ok,
236
+ registration: registration.state,
237
+ });
238
+ } catch (error) {
239
+ if (isAlive(child.pid)) {
240
+ try {
241
+ killFn(child.pid, "SIGTERM");
242
+ } catch {
243
+ // Best-effort: the wait error is what the caller should see.
244
+ }
245
+ }
246
+ clearDaemonState(root);
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ export async function runAttachControl(control, args, options = {}) {
252
+ const root = options.root ?? projectRoot(options.env);
253
+ const isAlive = options.isAlive ?? isProcessAlive;
254
+ if (control === "status") {
255
+ const state = readDaemonState(root);
256
+ if (!state) {
257
+ return { daemon: false, alive: false, pid: null, logPath: null };
258
+ }
259
+ return daemonReport(state, {
260
+ reused: true,
261
+ alive: Boolean(state.pid && isAlive(state.pid)),
262
+ });
263
+ }
264
+ if (control === "stop") {
265
+ return stopAttachDaemon({ root, isAlive, killFn: options.killFn });
266
+ }
267
+ return startAttachDaemon({ root, args, isAlive, ...options });
268
+ }
@@ -118,7 +118,7 @@ export const CLI_COMMANDS = Object.freeze([
118
118
  "Otherwise creates an isolated profile and writes .foldspace-dev/state.json",
119
119
  "Does not load the SDK or action artifact",
120
120
  ],
121
- next: ["Sign in to the target application", "Run foldspace attach"],
121
+ next: ["Sign in to the target application", "Observe the real workflow, or run foldspace attach"],
122
122
  }),
123
123
  Object.freeze({
124
124
  name: "attach",
@@ -127,7 +127,7 @@ export const CLI_COMMANDS = Object.freeze([
127
127
  summary:
128
128
  "Load local actions over CDP and observe the normal agent experience",
129
129
  usage:
130
- "foldspace attach [--bootstrap | --replace] [--target <name>] [--port <number>] [--agent <api-name>] [--no-test-mode] [--no-badge]",
130
+ "foldspace attach [--bootstrap | --replace] [--daemon | --status | --stop] [--target <name>] [--port <number>] [--agent <api-name>] [--no-test-mode] [--no-badge]",
131
131
  risk: "browser-session",
132
132
  environment: "local-chrome",
133
133
  environmentVariables: [
@@ -178,6 +178,12 @@ export const CLI_COMMANDS = Object.freeze([
178
178
  "Do not mark conversations as test traffic; use deliberately",
179
179
  ),
180
180
  flag("--no-badge", "Hide the visible Foldspace development badge"),
181
+ flag(
182
+ "--daemon",
183
+ "Start attach in the background and return after inspect_registration",
184
+ ),
185
+ flag("--status", "Print whether the attach daemon is running"),
186
+ flag("--stop", "Stop the attach daemon and restore prepared pages"),
181
187
  ],
182
188
  prerequisites: [
183
189
  "foldspace.dev.json",
@@ -191,13 +197,15 @@ export const CLI_COMMANDS = Object.freeze([
191
197
  "Never directly invokes an action handler",
192
198
  "An empty local action registry is valid; named actions are not required",
193
199
  "Restores prepared pages when detached cleanly",
200
+ "--daemon returns after inspect_registration and keeps CDP in a child process",
194
201
  ],
195
202
  next: [
203
+ "Coding agents should use foldspace attach --daemon so the invoking tool returns",
196
204
  "Exercise the visible agent normally",
197
- "Confirm [lifecycle] inspect_registration:registration_ok",
205
+ "Confirm [lifecycle] inspect_registration:registration_ok in the terminal or daemon log",
198
206
  "On mismatch, read foldspace help attach --json diagnostics and the lifecycle details",
199
- "Confirm execute/render observations when verifying a named action",
200
- "Detach with Ctrl-C",
207
+ "After a named action, read [actions] execute/render lines from the attach log",
208
+ "Detach with Ctrl-C, or foldspace attach --stop for a daemon",
201
209
  ],
202
210
  }),
203
211
  Object.freeze({
@@ -308,6 +316,7 @@ export function createCliRegistry({ packageName, packageVersion }) {
308
316
  "Action verification is passive and agent-driven.",
309
317
  "The harness bin alias is equivalent to foldspace.",
310
318
  "attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
319
+ "Coding agents should run attach --daemon; foreground attach is for humans watching the terminal.",
311
320
  ],
312
321
  };
313
322
  }
@@ -15,22 +15,31 @@ changes there.
15
15
  3. Inspect the selected agent with `get_agent_settings`, `list_actions`, and
16
16
  `list_task_agents`.
17
17
  4. Use `discover_actions` to identify candidate experiences, then let the user
18
- choose what to build.
18
+ choose what to build. Treat those ideas as candidates, not an inventory.
19
19
 
20
20
  ## Build workflow
21
21
 
22
22
  An experience can require several reusable actions. Separate lookup actions
23
23
  from actions that read or mutate a selected resource.
24
24
 
25
- 1. Agree on the experience and how its actions compose.
26
- 2. Create the action metadata as a draft.
27
- 3. Ask before publishing when the agent has real users; publishing is a live
28
- product change.
29
- 4. Observe the real target workflow and capture its browser requests.
30
- 5. Generate the typed handler skeleton from the published action schema.
31
- 6. Implement the handler using the observed request and response shapes.
32
- 7. Register the handler in `agent/actions/index.ts`.
33
- 8. Complete the verification gates below.
25
+ 1. Agree on the candidate experience and how its actions would compose. This is
26
+ not a publish and not a commitment.
27
+ 2. Run `npm run inject`. Ask the user to sign in and perform the real target
28
+ workflow in that Chrome window.
29
+ 3. Capture at least one real HTTP 200 for the data the action needs. Use
30
+ chrome-devtools MCP against the inject Chrome, or a page-context `fetch`,
31
+ **before** `attach` owns the debug port. If there is no 200,
32
+ pivot; do not create Foldspace resources.
33
+ 4. Create action metadata as a draft. `generate_action_handler` works from the
34
+ draft schema; do not publish yet.
35
+ 5. Implement the handler using the observed request and response shapes.
36
+ 6. Register the handler in `agent/actions/index.ts` and build.
37
+ 7. Run `npx foldspace attach --daemon` (add `--bootstrap` or `--replace` when
38
+ the page requires it). An empty local registry is valid if you only want to
39
+ see how the agent works.
40
+ 8. Ask before publishing. Publishing is required only so the copilot can call
41
+ the action, and it is a live product change when the agent has real users.
42
+ 9. Complete the verification gates below.
34
43
 
35
44
  ## API rules
36
45
 
@@ -38,8 +47,8 @@ Actions execute in the user's signed-in browser session.
38
47
 
39
48
  - Use the same internal APIs as the product page, with
40
49
  `credentials: "include"`.
41
- - Never guess endpoints or schemas. Capture at least one real request and
42
- response before implementing a parser.
50
+ - Never guess endpoints or schemas. Capture at least one real 200 before
51
+ implementing a parser. Do not implement a path that was not observed.
43
52
  - Verify that the user is signed in before observing a workflow.
44
53
  - Do not substitute a public developer API when the browser session is missing;
45
54
  ask the user to sign in.
@@ -50,14 +59,17 @@ Actions execute in the user's signed-in browser session.
50
59
  ```bash
51
60
  npm run dev
52
61
  npm run inject
53
- npm run attach
62
+ npx foldspace attach --daemon
54
63
  ```
55
64
 
56
65
  `inject` launches an isolated Chrome profile and records its debug port.
57
- It does not generate or load an application extension. `attach` prepares the
58
- page and loads the local `dist/index.js` bundle through CDP. An empty local
59
- registry is valid: attach before implementing handlers to see how the agent
60
- works. `npm run build` is still required so `dist/index.js` exists.
66
+ It does not generate or load an application extension. Observe the customer's
67
+ workflow after inject and **before** attach, while chrome-devtools MCP can use
68
+ the same Chrome. `attach` prepares the page and loads the local `dist/index.js`
69
+ bundle through CDP. Coding agents must use `--daemon` so the invoking tool
70
+ returns after `[lifecycle] inspect_registration:…`; foreground `npm run attach`
71
+ is for humans watching the terminal. An empty local registry is valid. `npm run
72
+ build` is still required so `dist/index.js` exists.
61
73
 
62
74
  Use the default swap only when the page already has the configured product and
63
75
  agent. Use `--bootstrap` only when the page has no Foldspace SDK, and
@@ -71,8 +83,15 @@ local registry is empty. On `registration_mismatch`, read
71
83
  lifecycle details (`missingActionNames`, `unexpectedActionNames`,
72
84
  `diagnosticError`).
73
85
 
74
- Prove a named action through the visible agent and require both an SDK action
75
- callback and local execute/render evidence; do not invoke the handler directly.
86
+ While attach is running it owns the debug port. Detach (`attach --stop`, or
87
+ Ctrl-C in the foreground) before using chrome-devtools MCP against the same
88
+ Chrome.
89
+
90
+ Prove a named action through the visible agent. After the user talks to the
91
+ copilot, read the daemon log for `[actions]` SDK callback and local
92
+ execute/render lines; do not invoke the handler directly. Those lines record
93
+ names, statuses, durations, and parameter keys only — not results or error
94
+ bodies.
76
95
 
77
96
  ## Verification gates
78
97
 
@@ -82,7 +101,9 @@ Do not report success without all six:
82
101
  2. The expected handler appears in `dist/index.js`.
83
102
  3. The browser reports `inspect_registration:registration_ok`. For a named
84
103
  action, the captured registry includes that handler.
85
- 4. The action behaves correctly against the real target workflow.
104
+ 4. The action behaves correctly against the real target workflow. Confirm
105
+ `[actions] local-handler:execute` in the attach log after the user exercises
106
+ the visible agent.
86
107
  5. Existing neighbouring action fixtures still pass when fixtures exist.
87
108
  6. Browser evidence came from the live target, not from hand-authored examples.
88
109
 
@@ -50,12 +50,17 @@ npm run build # create dist/index.js
50
50
  npm run dev # rebuild dist/index.js on changes
51
51
  npm run inject # launch the dedicated Chrome profile
52
52
  npm run attach # load local actions and observe the agent over CDP
53
+ npm run attach:daemon # same, but return after inspect_registration
53
54
  ```
54
55
 
55
56
  Run `inject` before `attach`. Sign in to the product in the Chrome window that
56
- `inject` opens. `inject` does not generate an application extension; `attach`
57
- loads `dist/index.js` directly through CDP. An empty local registry is valid,
58
- so you can attach before implementing handlers.
57
+ `inject` opens. Observe the real product workflow in that window **before**
58
+ attach if you need customer API evidence; `attach` owns the debug port.
59
+ `inject` does not generate an application extension; `attach` loads
60
+ `dist/index.js` directly through CDP. An empty local registry is valid,
61
+ so you can attach before implementing handlers. Coding agents should use
62
+ `attach --daemon` (or `npm run attach:daemon`) so the command returns after
63
+ registration instead of occupying the terminal.
59
64
 
60
65
  Choose the attach mode from the state of the target page:
61
66
 
@@ -69,10 +74,13 @@ agents can read the versioned contract with `npx foldspace help --json`.
69
74
 
70
75
  ## Add an action
71
76
 
72
- 1. Define and publish the action in Agent Studio or through Product MCP.
73
- 2. Copy `agent/actions/_example.ts` to `agent/actions/<action_key>.ts`.
74
- 3. Implement the handler from observed browser API evidence.
75
- 4. Import and register the handler in `agent/actions/index.ts`.
76
- 5. Type-check, rebuild, attach, and verify it in the target product.
77
+ 1. Observe the real product workflow in the inject Chrome and capture a real
78
+ 200 for the API the action will call.
79
+ 2. Create the action as a draft in Agent Studio or through Product MCP.
80
+ 3. Copy `agent/actions/_example.ts` to `agent/actions/<action_key>.ts`.
81
+ 4. Implement the handler from that observed request and response.
82
+ 5. Import and register the handler in `agent/actions/index.ts`.
83
+ 6. Type-check, rebuild, `attach --daemon`, and verify it through the visible
84
+ agent. Publish only when the copilot must call the action.
77
85
 
78
- Action keys and parameter names must exactly match the published action schema.
86
+ Action keys and parameter names must exactly match the action schema.
@@ -8,7 +8,8 @@
8
8
  "dev": "foldspace build --watch",
9
9
  "build": "foldspace build",
10
10
  "inject": "foldspace inject",
11
- "attach": "foldspace attach"
11
+ "attach": "foldspace attach",
12
+ "attach:daemon": "foldspace attach --daemon"
12
13
  },
13
14
  "devDependencies": {
14
15
  "@foldspace_npm/harness": "{{HARNESS_VERSION}}",