@kontextmind/kxm 0.7.53 → 0.7.55
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +42 -1
- package/docs/agent-skills.md +1 -1
- package/docs/configuration.md +0 -3
- package/docs/contracts/lifecycles.md +16 -0
- package/docs/contracts/migration.md +50 -258
- package/docs/contracts/validation.md +15 -33
- package/docs/test-matrix.md +2 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +376 -2133
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +18 -98
- package/plugins/kxm/dist/runtime.js +50 -114
- package/plugins/kxm/dist/server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +4 -7
- package/plugins/kxm/src/autocomplete.ts +0 -3
- package/plugins/kxm/src/cli/project.ts +2 -133
- package/plugins/kxm/src/cli.ts +3 -24
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/init.ts +10 -6
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/pi-producer.ts +61 -17
- package/plugins/kxm/src/project-config.ts +54 -173
- package/schemas/README.md +0 -3
- package/plugins/kxm/src/migrate.ts +0 -1848
- package/schemas/migration-decision.schema.json +0 -26
- package/schemas/migration-plan.schema.json +0 -123
- package/schemas/migration-receipt.schema.json +0 -52
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type KxmInitializationPlan,
|
|
13
13
|
} from "../project-config.ts";
|
|
14
14
|
import { initializeKxmProject } from "../init.ts";
|
|
15
|
-
import { applyKxmMigration, planKxmMigration, verifyKxmMigration } from "../migrate.ts";
|
|
16
15
|
import { diffKxmProjectAgainstRevision, formatKxmPermissionDiff } from "../permission.ts";
|
|
17
16
|
import { readKxmLocalBindings, kxmUserStateRoot } from "../bindings.ts";
|
|
18
17
|
import { loadKxmProject } from "../project-config.ts";
|
|
@@ -151,8 +150,8 @@ export async function cmdKxmInit(
|
|
|
151
150
|
if (runtime.dryRun) {
|
|
152
151
|
return finishInit(0, `init plan: ${initialized.plan.mode}`);
|
|
153
152
|
}
|
|
154
|
-
const next = initialized.plan.mode === "
|
|
155
|
-
? "legacy state
|
|
153
|
+
const next = initialized.plan.mode === "legacy"
|
|
154
|
+
? "legacy state is not migrated by this build: initialise a fresh project directory and copy the YAML definitions you want to keep"
|
|
156
155
|
: initialized.repairPlan?.issues.length
|
|
157
156
|
? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
|
|
158
157
|
: "partial or provenance-free KXM state requires explicit repair; no files were overwritten";
|
|
@@ -176,136 +175,6 @@ export async function cmdKxmInit(
|
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
|
|
179
|
-
export async function cmdKxmMigratePlan(runtime: Runtime): Promise<number> {
|
|
180
|
-
if (runtime.workspaceFlag !== undefined) {
|
|
181
|
-
print(runtime.io, runtime.json, {
|
|
182
|
-
ok: false,
|
|
183
|
-
command: "migrate plan",
|
|
184
|
-
error: "workspace_option_unsupported",
|
|
185
|
-
}, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
186
|
-
return 2;
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
const result = planKxmMigration(runtime.cwd, {});
|
|
190
|
-
const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
|
|
191
|
-
const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
|
|
192
|
-
const payload = {
|
|
193
|
-
ok: result.plan.canApply === true,
|
|
194
|
-
command: "migrate plan",
|
|
195
|
-
plan: result.plan,
|
|
196
|
-
plannedOnly: result.plan.canApply !== true,
|
|
197
|
-
};
|
|
198
|
-
if (result.plan.canApply === true) {
|
|
199
|
-
print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
|
|
200
|
-
return 0;
|
|
201
|
-
}
|
|
202
|
-
print(
|
|
203
|
-
runtime.io,
|
|
204
|
-
runtime.json,
|
|
205
|
-
payload,
|
|
206
|
-
`migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
|
|
207
|
-
);
|
|
208
|
-
return 1;
|
|
209
|
-
} catch (error) {
|
|
210
|
-
if (error instanceof KxmConfigError) {
|
|
211
|
-
print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
|
|
212
|
-
return 1;
|
|
213
|
-
}
|
|
214
|
-
print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_io_failed" }, "migration plan failed because a local filesystem operation did not complete");
|
|
215
|
-
return 1;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export async function cmdKxmMigrateApply(runtime: Runtime, options: { decisions?: string | undefined; projectId?: string | undefined; name?: string | undefined }): Promise<number> {
|
|
220
|
-
if (runtime.workspaceFlag !== undefined) {
|
|
221
|
-
print(runtime.io, runtime.json, {
|
|
222
|
-
ok: false,
|
|
223
|
-
command: "migrate apply",
|
|
224
|
-
error: "workspace_option_unsupported",
|
|
225
|
-
}, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
226
|
-
return 2;
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
const result = applyKxmMigration(runtime.cwd, {
|
|
230
|
-
...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
|
|
231
|
-
...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
|
|
232
|
-
...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
|
|
233
|
-
localStateRoot: kxmUserStateRoot({ env: runtime.env }),
|
|
234
|
-
dryRun: runtime.dryRun,
|
|
235
|
-
});
|
|
236
|
-
const payload = {
|
|
237
|
-
ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
|
|
238
|
-
command: "migrate apply",
|
|
239
|
-
action: result.action,
|
|
240
|
-
files: result.files,
|
|
241
|
-
...(result.configRevision ? { configRevision: result.configRevision } : {}),
|
|
242
|
-
...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
|
|
243
|
-
plannedOnly: result.action === "planned",
|
|
244
|
-
};
|
|
245
|
-
if (result.action === "applied") {
|
|
246
|
-
print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
|
|
247
|
-
return 0;
|
|
248
|
-
}
|
|
249
|
-
if (result.action === "already-migrated") {
|
|
250
|
-
print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
|
|
251
|
-
return 0;
|
|
252
|
-
}
|
|
253
|
-
if (runtime.dryRun && result.plan?.canApply === true) {
|
|
254
|
-
print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
|
|
255
|
-
return 0;
|
|
256
|
-
}
|
|
257
|
-
const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
|
|
258
|
-
print(
|
|
259
|
-
runtime.io,
|
|
260
|
-
runtime.json,
|
|
261
|
-
{ ...payload, plan: result.plan },
|
|
262
|
-
`migration blocked by ${ambiguities.length} unresolved decision(s); review 'kxm migrate plan' and pass --decisions:\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
|
|
263
|
-
);
|
|
264
|
-
return 1;
|
|
265
|
-
} catch (error) {
|
|
266
|
-
if (error instanceof KxmConfigError) {
|
|
267
|
-
print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
|
|
268
|
-
return 1;
|
|
269
|
-
}
|
|
270
|
-
print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_io_failed" }, "migration apply failed because a local filesystem operation did not complete");
|
|
271
|
-
return 1;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export async function cmdKxmMigrateVerify(runtime: Runtime): Promise<number> {
|
|
276
|
-
if (runtime.workspaceFlag !== undefined) {
|
|
277
|
-
print(runtime.io, runtime.json, {
|
|
278
|
-
ok: false,
|
|
279
|
-
command: "migrate verify",
|
|
280
|
-
error: "workspace_option_unsupported",
|
|
281
|
-
}, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
|
|
282
|
-
return 2;
|
|
283
|
-
}
|
|
284
|
-
let result: ReturnType<typeof verifyKxmMigration>;
|
|
285
|
-
try {
|
|
286
|
-
result = verifyKxmMigration(runtime.cwd, {});
|
|
287
|
-
} catch (error) {
|
|
288
|
-
if (error instanceof KxmConfigError) {
|
|
289
|
-
print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
|
|
290
|
-
return 1;
|
|
291
|
-
}
|
|
292
|
-
print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_io_failed" }, "migration verification failed because a local filesystem operation did not complete");
|
|
293
|
-
return 1;
|
|
294
|
-
}
|
|
295
|
-
const payload = {
|
|
296
|
-
ok: result.ok,
|
|
297
|
-
command: "migrate verify",
|
|
298
|
-
...(result.configRevision ? { configRevision: result.configRevision } : {}),
|
|
299
|
-
issues: result.issues,
|
|
300
|
-
};
|
|
301
|
-
if (result.ok) {
|
|
302
|
-
print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
|
|
303
|
-
return 0;
|
|
304
|
-
}
|
|
305
|
-
print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
|
|
306
|
-
return 1;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
178
|
export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined }): Promise<number> {
|
|
310
179
|
try {
|
|
311
180
|
const { manifest, outDir } = createBackup({
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Role & workflow governance (`kxm role`, `kxm workflow`, `kxm gate`, `kxm signal`)
|
|
8
8
|
* - Task & goal management (`kxm goal`, `kxm task`, `kxm suggest`, `kxm studio`)
|
|
9
9
|
* - Knowledge, skills, and memory (`kxm context`, `kxm skills`, `kxm memory`)
|
|
10
|
-
* - KXM runtime & initialization (`kxm init`, `kxm
|
|
10
|
+
* - KXM runtime & initialization (`kxm init`, `kxm trust`, `kxm run`, `kxm harness`)
|
|
11
11
|
* - Hub, workers, and dashboard (`kxm hub`, `kxm worker`, `kxm dash`, `kxm session`, `kxm auth`)
|
|
12
12
|
* - System, update, and configuration (`kxm update`, `kxm config`, `kxm completion`, `kxm improve`)
|
|
13
13
|
*/
|
|
@@ -98,10 +98,7 @@ import {
|
|
|
98
98
|
|
|
99
99
|
import {
|
|
100
100
|
cmdKxmInit,
|
|
101
|
-
|
|
102
|
-
cmdKxmMigrateApply,
|
|
103
|
-
cmdKxmMigrateVerify,
|
|
104
|
-
cmdBackup,
|
|
101
|
+
cmdBackup,
|
|
105
102
|
cmdRestore,
|
|
106
103
|
cmdKxmTrust,
|
|
107
104
|
cmdKxmRun,
|
|
@@ -334,7 +331,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
334
331
|
.helpCommand("help", "Show help");
|
|
335
332
|
addGlobalOptions(program);
|
|
336
333
|
|
|
337
|
-
program.command("init").description("Create, validate, or
|
|
334
|
+
program.command("init").description("Create, validate, repair, or join a KXM project")
|
|
338
335
|
.option("--json", "Print machine-readable JSON")
|
|
339
336
|
.option("--dry-run", "Plan without making changes")
|
|
340
337
|
.option("--name <name>", "Project display name for a new project")
|
|
@@ -347,24 +344,6 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
347
344
|
});
|
|
348
345
|
});
|
|
349
346
|
|
|
350
|
-
const migrate = addGlobalOptions(program.command("migrate").description("Plan, apply, and verify legacy JSON configuration migration"));
|
|
351
|
-
migrate.helpCommand("help", "Show migrate help");
|
|
352
|
-
addGlobalOptions(migrate.command("plan").description("Compute the deterministic legacy-to-KXM migration plan without writes"))
|
|
353
|
-
.action(async function migratePlanAction(this: Command) {
|
|
354
|
-
result.code = await cmdKxmMigratePlan(runtimeFrom(ctx, this));
|
|
355
|
-
});
|
|
356
|
-
addGlobalOptions(migrate.command("apply").description("Install a reviewed migration with a hash-linked receipt"))
|
|
357
|
-
.option("--decisions <file>", "Reviewed kxm.migration-decision.v1 YAML file")
|
|
358
|
-
.option("--project-id <id>", "Stable project ID for controlled provisioning")
|
|
359
|
-
.option("--name <name>", "Project display name")
|
|
360
|
-
.action(async function migrateApplyAction(this: Command, options: { decisions?: string; projectId?: string; name?: string }) {
|
|
361
|
-
result.code = await cmdKxmMigrateApply(runtimeFrom(ctx, this), options);
|
|
362
|
-
});
|
|
363
|
-
addGlobalOptions(migrate.command("verify").description("Verify a migration receipt against current sources and target bundle"))
|
|
364
|
-
.action(async function migrateVerifyAction(this: Command) {
|
|
365
|
-
result.code = await cmdKxmMigrateVerify(runtimeFrom(ctx, this));
|
|
366
|
-
});
|
|
367
|
-
|
|
368
347
|
addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of all stores with a hashed manifest"))
|
|
369
348
|
.option("--out <dir>", "Directory to write backup and manifest")
|
|
370
349
|
.action(async function backupAction(this: Command, options: { out?: string }) {
|
|
@@ -179,7 +179,7 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
|
|
|
179
179
|
throw databaseError(
|
|
180
180
|
"runtime_schema_outdated",
|
|
181
181
|
file,
|
|
182
|
-
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (
|
|
182
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for registry/event stores); \`kxm init\` is project-only and rebuilds no database — upgrading old state in place is deliberately unsupported`,
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
|
package/plugins/kxm/src/init.ts
CHANGED
|
@@ -207,9 +207,18 @@ function initializeKxmProjectAtGitRoot(
|
|
|
207
207
|
const loaderOptions = configOptions(options, repositoryBindings);
|
|
208
208
|
const transactionOptions = repairOptions(options, repositoryBindings);
|
|
209
209
|
|
|
210
|
+
const plan = planKxmInitialization(start, loaderOptions);
|
|
211
|
+
if (plan.mode === "legacy") {
|
|
212
|
+
// Classified **before** any transaction or lock work. A legacy tree used to reach the
|
|
213
|
+
// recovery branch first, so an interrupted create/repair journal made ordinary init
|
|
214
|
+
// acquire the project mutation lock, clean or resume the transaction, and write files
|
|
215
|
+
// while reporting `mode: "legacy"`. Nothing is resumed, cleaned, or written here: the
|
|
216
|
+
// pending journal stays exactly as it is until the legacy inputs are dealt with.
|
|
217
|
+
return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
210
220
|
if (hasKxmInitTransaction(gitRoot)) {
|
|
211
221
|
const operation = inspectKxmInitTransaction(gitRoot, options.schemasDir);
|
|
212
|
-
const plan = planKxmInitialization(start, loaderOptions);
|
|
213
222
|
if (!operation) {
|
|
214
223
|
if (options.dryRun) return { action: "planned", plan, projectRoot: gitRoot, resumePending: true, files: [] };
|
|
215
224
|
if (!mutationLock) throw new Error("project mutation lock is required to clean an empty transaction");
|
|
@@ -267,11 +276,6 @@ function initializeKxmProjectAtGitRoot(
|
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
278
|
|
|
270
|
-
const plan = planKxmInitialization(start, loaderOptions);
|
|
271
|
-
if (plan.mode === "migrate") {
|
|
272
|
-
return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
279
|
if (plan.mode === "repair") {
|
|
276
280
|
const projectRoot = plan.projectRoot ?? gitRoot;
|
|
277
281
|
let templateRepair: KxmTemplateRepairPlan | undefined;
|
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.55";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -82,20 +82,62 @@ function extractText(content: unknown): string {
|
|
|
82
82
|
return "";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
function asJsonObject(text: string): Record<string, unknown> | undefined {
|
|
86
|
+
try {
|
|
87
|
+
const parsed: unknown = JSON.parse(text);
|
|
88
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function outcomeField(value: Record<string, unknown>): string | undefined {
|
|
95
|
+
const outcome = value.outcome;
|
|
96
|
+
return typeof outcome === "string" ? outcome : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The result a reply actually declares, or `undefined` when it declares none.
|
|
101
|
+
*
|
|
102
|
+
* Two shapes count: the whole reply is one JSON object, or a **standalone** result object on a
|
|
103
|
+
* line of its own — and when there is more than one of those, the **last** one wins, because
|
|
104
|
+
* that is where a reply puts its answer after showing an example. If anything in the tail after
|
|
105
|
+
* that declaration still looks like an outcome key, the reply is **ambiguous and settles
|
|
106
|
+
* `failed`**. What is deliberately not accepted: an outcome *word* anywhere in prose, and an
|
|
107
|
+
* object embedded mid-sentence, so
|
|
108
|
+
* `Example: {"outcome": "passed"}. Actual result: {"outcome": "failed"}` declares nothing at
|
|
109
|
+
* all and settles as `failed` rather than letting the illustration outrank the answer.
|
|
110
|
+
*/
|
|
111
|
+
function declaredOutcomeOf(text: string): string | undefined {
|
|
112
|
+
const trimmed = text.trim();
|
|
113
|
+
if (!trimmed) return undefined;
|
|
114
|
+
const whole = asJsonObject(trimmed);
|
|
115
|
+
if (whole) return outcomeField(whole);
|
|
116
|
+
const lines = trimmed.split(/\r?\n/);
|
|
117
|
+
let declaration: { index: number; outcome: string } | undefined;
|
|
118
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
119
|
+
const outcome = outcomeField(asJsonObject(lines[index]!.trim()) ?? {});
|
|
120
|
+
if (outcome !== undefined) declaration = { index, outcome };
|
|
96
121
|
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
122
|
+
if (!declaration) return undefined;
|
|
123
|
+
// Ambiguity after the declaration fails closed. Anything in the tail that still looks like
|
|
124
|
+
// an outcome key — an inline `Actual result: {"outcome": "failed"}` on the next line, a
|
|
125
|
+
// pretty-printed object, or a second mention — means we cannot tell which one the reply is
|
|
126
|
+
// reporting, and guessing is exactly the behaviour this function exists to remove. Trailing
|
|
127
|
+
// prose that says nothing about outcomes is fine, which is what lets a real reply put its
|
|
128
|
+
// usage or sign-off after the result block.
|
|
129
|
+
const tail = lines.slice(declaration.index + 1).join("\n");
|
|
130
|
+
if (/"outcome"\s*:/.test(tail)) return undefined;
|
|
131
|
+
return declaration.outcome;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function determineOutcome(text: string, allowedOutcomes: readonly string[]): string {
|
|
135
|
+
// Structured result only. What does **not** count: an outcome *word* anywhere in the text —
|
|
136
|
+
// "the tests did not pass" used to settle a step as `passed` — and an empty or unstructured
|
|
137
|
+
// reply, which used to default to success. Undeclared here means `failed`; if the step does
|
|
138
|
+
// not declare `failed` the engine records `outcome_unknown` and terminates as `failed` anyway.
|
|
139
|
+
const declared = declaredOutcomeOf(text);
|
|
140
|
+
return declared !== undefined && allowedOutcomes.includes(declared) ? declared : "failed";
|
|
99
141
|
}
|
|
100
142
|
|
|
101
143
|
export class PiSession {
|
|
@@ -255,9 +297,11 @@ export class PiSession {
|
|
|
255
297
|
const isAborted = active.aborted || active.signal?.aborted;
|
|
256
298
|
let outcome: string;
|
|
257
299
|
if (isAborted) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
300
|
+
// A cancel is a cancel. The old chain fell through to `allowedOutcomes[0]` when the
|
|
301
|
+
// step declared neither `cancelled` nor `failed`, so aborting a step whose only
|
|
302
|
+
// declared outcome was `passed` reported `passed`. Returning `cancelled` instead lets
|
|
303
|
+
// the engine record `outcome_unknown` and terminate `failed` — never a borrowed success.
|
|
304
|
+
outcome = "cancelled";
|
|
261
305
|
} else {
|
|
262
306
|
outcome = determineOutcome(active.text, active.allowedOutcomes);
|
|
263
307
|
}
|
|
@@ -316,8 +360,8 @@ export class PiSession {
|
|
|
316
360
|
throw new Error("pi_session_busy");
|
|
317
361
|
}
|
|
318
362
|
if (signal?.aborted) {
|
|
319
|
-
|
|
320
|
-
return { outcome, text: "aborted", usage: {} };
|
|
363
|
+
// Same rule as the in-flight abort: never fall back to the first declared outcome.
|
|
364
|
+
return { outcome: "cancelled", text: "aborted", usage: {} };
|
|
321
365
|
}
|
|
322
366
|
|
|
323
367
|
this.status = "busy";
|