@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 +10 -7
- package/bin/attach.mjs +17 -15
- package/bin/cli.mjs +16 -9
- package/package.json +1 -1
- package/src/attach-helpers.mjs +15 -0
- package/src/cli-help.mjs +4 -0
- package/src/cli-registry.mjs +8 -4
- package/src/init.mjs +18 -7
- package/templates/agent-starter/CLAUDE.md +13 -5
- package/templates/agent-starter/README.md +2 -1
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-
|
|
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-
|
|
108
|
+
--agent-key my-agent \
|
|
109
109
|
--domain app.example.com
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
144
|
-
and then observes action callbacks while
|
|
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
|
|
298
|
-
|
|
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.
|
|
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.
|
|
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)}
|
|
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
|
-
|
|
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
|
|
85
|
-
if (!
|
|
86
|
-
|
|
87
|
-
seen.
|
|
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(
|
|
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(
|
|
107
|
+
normalized.push(canonicalName, optionValue);
|
|
101
108
|
}
|
|
102
109
|
return normalized;
|
|
103
110
|
}
|
package/package.json
CHANGED
package/src/attach-helpers.mjs
CHANGED
|
@@ -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));
|
package/src/cli-registry.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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,
|
|
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
|
-
"
|
|
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([
|
|
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-
|
|
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
|
|
212
|
-
(value) => validateIdentifier(value, "Agent
|
|
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-
|
|
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
|
|
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.
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|