@foldspace_npm/harness 0.1.3 → 0.1.5

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
@@ -95,7 +95,7 @@ Non-interactive / CI form:
95
95
  ```bash
96
96
  npx --yes @foldspace_npm/harness init my-agent \
97
97
  --product-id FR8JUQZAQRZB \
98
- --agent-api-name my-agent \
98
+ --agent-key my-agent \
99
99
  --domain app.example.com \
100
100
  --name "My Agent"
101
101
  ```
@@ -105,12 +105,14 @@ When running directly from a harness checkout during development:
105
105
  ```bash
106
106
  node bin/cli.mjs init ../my-agent \
107
107
  --product-id FR8JUQZAQRZB \
108
- --agent-api-name my-agent \
108
+ --agent-key my-agent \
109
109
  --domain app.example.com
110
110
  ```
111
111
 
112
- `--name` is optional and defaults to the target directory name. The product ID
113
- must be the bare ID, not an `EU-…` SDK key. The domain may be a hostname or an
112
+ The Agent Key is the value shown in Agent Studio, such as `my-agent`.
113
+ `--agent-api-name` remains a deprecated alias for `--agent-key`. `--name` is
114
+ optional and defaults to the target directory name. The product ID must be the
115
+ bare ID, not the `EU-…-1-1` SDK loader key. The domain may be a hostname or an
114
116
  HTTP(S) URL without a port or path.
115
117
 
116
118
  For safety, `init` requires a target path that does not exist. It does not
@@ -140,9 +142,10 @@ Use `foldspace help attach` for mode requirements, effects, and safety options.
140
142
  ### Verify actions through the agent
141
143
 
142
144
  `foldspace attach` does not invoke action handlers directly. It loads the local
143
- artifact, verifies that its action names exactly match the configured SDK agent,
144
- and then observes action callbacks while you exercise the normal agent
145
- experience.
145
+ artifact, verifies that the captured local registry matches the configured SDK
146
+ agent — including an empty registry — and then observes action callbacks while
147
+ you exercise the normal agent experience. Named actions are not required to
148
+ attach.
146
149
 
147
150
  Action output identifies SDK callbacks and local `execute`/`render` phases:
148
151
 
package/bin/attach.mjs CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  hostMatches,
31
31
  hostPatternsFromTarget,
32
32
  parseAgentId,
33
+ registrationVerified,
33
34
  } from "../src/attach-helpers.mjs";
34
35
  import {
35
36
  buildReplacePrelude,
@@ -294,8 +295,10 @@ const sessionCollector = createSessionCollector();
294
295
  // sessionId -> preparation state used for deterministic detach cleanup.
295
296
  const prepared = new Map();
296
297
  const preparing = new Set();
297
- // Rejections are scoped to the observed URL so a later navigation can retry.
298
- const rejected = new Map();
298
+ // Rejections are scoped to the CDP session. SPA URL changes must not retry
299
+ // prepare() on the same tab. Off-host navigation still clears rejected in
300
+ // targetInfoChanged so a later return to the app can retry.
301
+ const rejected = new Set();
299
302
  // targetId -> sessionId, so a target that navigates INTO a matching host can be
300
303
  // prepared later. Without this, opening Chrome on a new tab and then browsing
301
304
  // to the app never arms the swap.
@@ -451,13 +454,7 @@ async function verifyPreparedPage(ws, sessionId) {
451
454
  },
452
455
  );
453
456
  latest.registration = registration;
454
- if (
455
- observation?.captureCount > 0 &&
456
- expectedActionNames.length > 0 &&
457
- registration?.agentFound &&
458
- registration.missingActionNames?.length === 0 &&
459
- registration.unexpectedActionNames?.length === 0
460
- ) {
457
+ if (registrationVerified(observation, registration)) {
461
458
  return {
462
459
  ok: true,
463
460
  state,
@@ -493,9 +490,6 @@ async function verifyPreparedPage(ws, sessionId) {
493
490
  }
494
491
 
495
492
  async function prepare(ws, sessionId, url) {
496
- if (rejected.has(sessionId) && rejected.get(sessionId) !== url) {
497
- rejected.delete(sessionId);
498
- }
499
493
  if (
500
494
  prepared.has(sessionId) ||
501
495
  preparing.has(sessionId) ||
@@ -527,7 +521,7 @@ async function prepare(ws, sessionId, url) {
527
521
  pageProductIds,
528
522
  });
529
523
  if (!guard.ok) {
530
- rejected.set(sessionId, url);
524
+ rejected.add(sessionId);
531
525
  logLifecycle(
532
526
  createLifecycleResult({
533
527
  operation: "prepare_page",
@@ -697,6 +691,11 @@ async function prepare(ws, sessionId, url) {
697
691
  verification.registration?.actionNames || [],
698
692
  expectedActionNames:
699
693
  verification.observation?.expectedActionNames || [],
694
+ expectedActionCount: Array.isArray(
695
+ verification.observation?.expectedActionNames,
696
+ )
697
+ ? verification.observation.expectedActionNames.length
698
+ : 0,
700
699
  missingActionNames:
701
700
  verification.registration?.missingActionNames || [],
702
701
  unexpectedActionNames:
@@ -715,7 +714,7 @@ async function prepare(ws, sessionId, url) {
715
714
  await cleanupPreparedSession(ws, sessionId, state);
716
715
  prepared.delete(sessionId);
717
716
  }
718
- rejected.set(sessionId, url);
717
+ rejected.add(sessionId);
719
718
  }
720
719
  }
721
720
  } finally {
@@ -920,7 +919,10 @@ console.log(`Mode: ${attachMode}`);
920
919
  console.log(`Agent: ${agentApiName} (product ${productId}, ${agentMode})`);
921
920
  console.log(`Test: ${noTestMode ? "OFF — conversations WILL appear in the dashboard" : "on"}`);
922
921
  console.log(`Hosts: ${hostPatterns.join(", ")}`);
923
- console.log(`Serving: ${path.relative(root, bundlePath)}\n`);
922
+ console.log(`Serving: ${path.relative(root, bundlePath)}`);
923
+ console.log(
924
+ `CDP: this process owns the debug port. Detach before using chrome-devtools MCP against the same Chrome.\n`,
925
+ );
924
926
 
925
927
  function printSessionSummary() {
926
928
  if (summaryPrinted) return;
package/bin/cli.mjs CHANGED
@@ -62,9 +62,13 @@ function printHelp(topic, json) {
62
62
  }
63
63
 
64
64
  function normalizeCommandArgs(command, args) {
65
- const options = new Map(
66
- command.options.map((option) => [option.name, option]),
67
- );
65
+ const options = new Map();
66
+ for (const option of command.options) {
67
+ options.set(option.name, { option, canonicalName: option.name });
68
+ for (const alias of option.deprecatedAliases || []) {
69
+ options.set(alias, { option, canonicalName: option.name });
70
+ }
71
+ }
68
72
  const seen = new Set();
69
73
  const normalized = [];
70
74
  let positionals = 0;
@@ -81,14 +85,17 @@ function normalizeCommandArgs(command, args) {
81
85
 
82
86
  const separator = token.indexOf("=");
83
87
  const name = separator > -1 ? token.slice(0, separator) : token;
84
- const option = options.get(name);
85
- if (!option) throw new Error(`unknown option '${name}'`);
86
- if (seen.has(name)) throw new Error(`option '${name}' was provided twice`);
87
- seen.add(name);
88
+ const match = options.get(name);
89
+ if (!match) throw new Error(`unknown option '${name}'`);
90
+ const { option, canonicalName } = match;
91
+ if (seen.has(canonicalName)) {
92
+ throw new Error(`option '${canonicalName}' was provided twice`);
93
+ }
94
+ seen.add(canonicalName);
88
95
 
89
96
  if (option.kind === "flag") {
90
97
  if (separator > -1) throw new Error(`option '${name}' takes no value`);
91
- normalized.push(name);
98
+ normalized.push(canonicalName);
92
99
  continue;
93
100
  }
94
101
 
@@ -97,7 +104,7 @@ function normalizeCommandArgs(command, args) {
97
104
  if (!optionValue || optionValue.startsWith("--")) {
98
105
  throw new Error(`option '${name}' requires a value`);
99
106
  }
100
- normalized.push(name, optionValue);
107
+ normalized.push(canonicalName, optionValue);
101
108
  }
102
109
  return normalized;
103
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldspace_npm/harness",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Build and verify portable Foldspace action artifacts against a live app.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -140,6 +140,21 @@ export function actionRequestMatchesTarget(request, target) {
140
140
  );
141
141
  }
142
142
 
143
+ export function registrationVerified(observation, registration) {
144
+ return Boolean(
145
+ observation?.installed === true &&
146
+ observation?.ownerMatches === true &&
147
+ observation?.actionNameLimitExceeded !== true &&
148
+ observation?.captureCount > 0 &&
149
+ Array.isArray(observation.expectedActionNames) &&
150
+ registration?.agentFound === true &&
151
+ Array.isArray(registration.missingActionNames) &&
152
+ registration.missingActionNames.length === 0 &&
153
+ Array.isArray(registration.unexpectedActionNames) &&
154
+ registration.unexpectedActionNames.length === 0,
155
+ );
156
+ }
157
+
143
158
  export function summarizeConsoleMessages(messages, limit = 50) {
144
159
  const summaries = new Map();
145
160
  for (const message of messages) {
package/src/cli-help.mjs CHANGED
@@ -74,6 +74,10 @@ export function renderCommandHelp(registry, name) {
74
74
  `${option.name}${option.value ? ` <${option.value}>` : ""}`,
75
75
  `${option.description}${
76
76
  option.default ? ` (default: ${option.default})` : ""
77
+ }${
78
+ option.deprecatedAliases?.length
79
+ ? ` (deprecated alias: ${option.deprecatedAliases.join(", ")})`
80
+ : ""
77
81
  }`,
78
82
  ]);
79
83
  sections.push("", "Options:", rows(optionRows));
@@ -21,7 +21,7 @@ export const CLI_COMMANDS = Object.freeze([
21
21
  group: "start",
22
22
  summary: "Create a configured Foldspace actions project",
23
23
  usage:
24
- "foldspace init [<directory>] [--product-id <id>] [--agent-api-name <name>] [--domain <host>] [--name <display-name>]",
24
+ "foldspace init [<directory>] [--product-id <id>] [--agent-key <key>] [--domain <host>] [--name <display-name>]",
25
25
  risk: "local-write",
26
26
  environment: "node",
27
27
  environmentVariables: [],
@@ -37,8 +37,9 @@ export const CLI_COMMANDS = Object.freeze([
37
37
  value("--product-id", "id", "Bare Foldspace product ID", {
38
38
  required: "non-interactive",
39
39
  }),
40
- value("--agent-api-name", "name", "Agent API name", {
40
+ value("--agent-key", "key", "Agent Key shown in Agent Studio", {
41
41
  required: "non-interactive",
42
+ deprecatedAliases: ["--agent-api-name"],
42
43
  }),
43
44
  value("--domain", "host", "Target hostname or HTTP(S) URL", {
44
45
  required: "non-interactive",
@@ -50,7 +51,7 @@ export const CLI_COMMANDS = Object.freeze([
50
51
  prerequisites: [
51
52
  "Node 20 or newer",
52
53
  "A target directory that does not already exist",
53
- "Non-interactive use requires directory, product ID, agent name, and domain",
54
+ "Non-interactive use requires directory, product ID, Agent Key, and domain",
54
55
  ],
55
56
  effects: ["Creates a new local project; never initializes Git"],
56
57
  next: [
@@ -188,12 +189,14 @@ export const CLI_COMMANDS = Object.freeze([
188
189
  "May reload and instrument matching target pages",
189
190
  "Test mode is enabled unless --no-test-mode is passed",
190
191
  "Never directly invokes an action handler",
192
+ "An empty local action registry is valid; named actions are not required",
191
193
  "Restores prepared pages when detached cleanly",
192
194
  ],
193
195
  next: [
194
196
  "Exercise the visible agent normally",
195
197
  "Confirm [lifecycle] inspect_registration:registration_ok",
196
- "Confirm execute/render observations",
198
+ "On mismatch, read foldspace help attach --json diagnostics and the lifecycle details",
199
+ "Confirm execute/render observations when verifying a named action",
197
200
  "Detach with Ctrl-C",
198
201
  ],
199
202
  }),
@@ -304,6 +307,7 @@ export function createCliRegistry({ packageName, packageVersion }) {
304
307
  "Capability metadata describes current behavior; it is not hosted-environment enforcement.",
305
308
  "Action verification is passive and agent-driven.",
306
309
  "The harness bin alias is equivalent to foldspace.",
310
+ "attach diagnostics are attach-internal; interpret them from the lifecycle log, not as CLI commands.",
307
311
  ],
308
312
  };
309
313
  }
package/src/init.mjs CHANGED
@@ -8,7 +8,13 @@ import { commandByName } from "./cli-registry.mjs";
8
8
 
9
9
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const defaultTemplateRoot = path.join(packageRoot, "templates", "agent-starter");
11
- const allowedFlags = new Set(["name", "product-id", "agent-api-name", "domain"]);
11
+ const allowedFlags = new Set([
12
+ "name",
13
+ "product-id",
14
+ "agent-key",
15
+ "agent-api-name",
16
+ "domain",
17
+ ]);
12
18
  const tokenPattern = /\{\{([A-Z0-9_]+)\}\}/g;
13
19
  const defaultDirectory = "my-agent";
14
20
 
@@ -75,12 +81,17 @@ export function parseInitArgs(argv) {
75
81
  if (positional.length > 1) {
76
82
  throw new Error(`Usage: ${initUsage}`);
77
83
  }
84
+ if (flags["agent-key"] && flags["agent-api-name"]) {
85
+ throw new Error(
86
+ "Use --agent-key only; --agent-api-name is its deprecated alias.",
87
+ );
88
+ }
78
89
 
79
90
  return {
80
91
  directory: positional[0] || null,
81
92
  displayName: flags.name || null,
82
93
  productId: flags["product-id"] || null,
83
- agentApiName: flags["agent-api-name"] || null,
94
+ agentApiName: flags["agent-key"] || flags["agent-api-name"] || null,
84
95
  domain: flags.domain || null,
85
96
  };
86
97
  }
@@ -89,7 +100,7 @@ function missingInitFields(parsed) {
89
100
  const missing = [];
90
101
  if (!parsed.directory) missing.push("directory");
91
102
  if (!parsed.productId) missing.push("product-id");
92
- if (!parsed.agentApiName) missing.push("agent-api-name");
103
+ if (!parsed.agentApiName) missing.push("agent-key");
93
104
  if (!parsed.domain) missing.push("domain");
94
105
  return missing;
95
106
  }
@@ -208,8 +219,8 @@ async function promptForMissingFields(parsed, options = {}) {
208
219
 
209
220
  if (!next.agentApiName) {
210
221
  next.agentApiName = await askUntilValid(
211
- "Agent API name",
212
- (value) => validateIdentifier(value, "Agent API name"),
222
+ "Agent Key",
223
+ (value) => validateIdentifier(value, "Agent Key"),
213
224
  options,
214
225
  );
215
226
  }
@@ -243,7 +254,7 @@ function finalizeInitConfig(parsed) {
243
254
 
244
255
  for (const [key, label] of [
245
256
  ["productId", "product-id"],
246
- ["agentApiName", "agent-api-name"],
257
+ ["agentApiName", "agent-key"],
247
258
  ["domain", "domain"],
248
259
  ]) {
249
260
  if (!parsed[key]) {
@@ -282,7 +293,7 @@ function createTemplateValues(config, harnessVersion) {
282
293
  }
283
294
 
284
295
  const productId = validateProductId(config.productId);
285
- const agentApiName = validateIdentifier(config.agentApiName, "Agent API name");
296
+ const agentApiName = validateIdentifier(config.agentApiName, "Agent Key");
286
297
  const target = normalizeTarget(config.domain);
287
298
  const hosts =
288
299
  target.domain === "localhost" || /^\d+(?:\.\d+){3}$/.test(target.domain)
@@ -55,7 +55,9 @@ npm run attach
55
55
 
56
56
  `inject` launches an isolated Chrome profile and records its debug port.
57
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.
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.
59
61
 
60
62
  Use the default swap only when the page already has the configured product and
61
63
  agent. Use `--bootstrap` only when the page has no Foldspace SDK, and
@@ -63,9 +65,14 @@ agent. Use `--bootstrap` only when the page has no Foldspace SDK, and
63
65
  present without the configured agent.
64
66
 
65
67
  The attach log must report `inspect_registration:registration_ok` before
66
- treating the page as registered. Prove the experience through the visible agent
67
- and require both an SDK action callback and local execute/render evidence; do
68
- not invoke the handler directly.
68
+ treating the page as registered. Zero captured actions is success when the
69
+ local registry is empty. On `registration_mismatch`, read
70
+ `npx foldspace help attach --json` diagnostics and map those names onto the
71
+ lifecycle details (`missingActionNames`, `unexpectedActionNames`,
72
+ `diagnosticError`).
73
+
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.
69
76
 
70
77
  ## Verification gates
71
78
 
@@ -73,7 +80,8 @@ Do not report success without all six:
73
80
 
74
81
  1. TypeScript compiles with `npx tsc --noEmit -p tsconfig.json`.
75
82
  2. The expected handler appears in `dist/index.js`.
76
- 3. The browser reports the expected number of attached actions.
83
+ 3. The browser reports `inspect_registration:registration_ok`. For a named
84
+ action, the captured registry includes that handler.
77
85
  4. The action behaves correctly against the real target workflow.
78
86
  5. Existing neighbouring action fixtures still pass when fixtures exist.
79
87
  6. Browser evidence came from the live target, not from hand-authored examples.
@@ -54,7 +54,8 @@ npm run attach # load local actions and observe the agent over CDP
54
54
 
55
55
  Run `inject` before `attach`. Sign in to the product in the Chrome window that
56
56
  `inject` opens. `inject` does not generate an application extension; `attach`
57
- loads `dist/index.js` directly through CDP.
57
+ loads `dist/index.js` directly through CDP. An empty local registry is valid,
58
+ so you can attach before implementing handlers.
58
59
 
59
60
  Choose the attach mode from the state of the target page:
60
61