akm-cli 0.9.11 → 0.9.12
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/CHANGELOG.md +166 -0
- package/STABILITY.md +6 -1
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +186 -71
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +2 -2
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +13 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +4 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +50 -4
- package/dist/core/config/config.js +21 -5
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +2 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +21 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +2 -3
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +12 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/integrations/agent/engine-resolution.js +15 -1
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +16 -0
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +5 -3
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1461 -1267
- package/dist/scripts/akm-migrate.js +1460 -1266
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +15 -1
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +19 -10
- package/dist/workflows/exec/step-work.js +35 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +76 -150
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +74 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -12,8 +12,8 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { stashDirNames } from "../../core/asset/asset-placement.js";
|
|
14
14
|
import { mutateConfig } from "../../core/config/config.js";
|
|
15
|
-
import { ConfigError } from "../../core/errors.js";
|
|
16
15
|
import { assertSafeStashDir, getConfigPath, getDefaultStashDir } from "../../core/paths.js";
|
|
16
|
+
import { warnOnce } from "../../core/warn.js";
|
|
17
17
|
import { primaryBundlePath, withPrimaryBundle } from "./bundle-config-ops.js";
|
|
18
18
|
import { copyStashSkeleton, ensureStashGitignore, scaffoldStashMeta } from "./stash-skeleton.js";
|
|
19
19
|
/**
|
|
@@ -37,11 +37,9 @@ import { copyStashSkeleton, ensureStashGitignore, scaffoldStashMeta } from "./st
|
|
|
37
37
|
function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
38
38
|
if (!dirExplicitlyProvided)
|
|
39
39
|
return; // Only guard explicit --dir, not default HOME resolution.
|
|
40
|
-
const isUnderTest =
|
|
40
|
+
const isUnderTest = process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
41
41
|
if (!isUnderTest)
|
|
42
42
|
return;
|
|
43
|
-
if (process.env.AKM_FORCE_INIT_TMP_STASH === "1")
|
|
44
|
-
return;
|
|
45
43
|
const isTmp = stashDir.startsWith("/tmp/") ||
|
|
46
44
|
stashDir === "/tmp" ||
|
|
47
45
|
stashDir.startsWith("/var/tmp/") ||
|
|
@@ -50,10 +48,8 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
|
50
48
|
stashDir.startsWith("/private/tmp/");
|
|
51
49
|
if (!isTmp)
|
|
52
50
|
return;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function isUnderTestRunner() {
|
|
56
|
-
return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
51
|
+
warnOnce(`init-tmp-stash:${stashDir}`, `Persisting --dir stashDir to a temporary path (${stashDir}) while a test-runner env var (BUN_TEST/NODE_ENV=test) is set; ` +
|
|
52
|
+
"the OS may reap this path, leaving the next run pointing at a deleted bundle.");
|
|
57
53
|
}
|
|
58
54
|
// ── Test seam ────────────────────────────────────────────────────────────────
|
|
59
55
|
// Swap-and-restore override. Inert in production; only tests call the setter.
|
|
@@ -346,8 +346,8 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
if (fs.existsSync(backupPath)) {
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
warn(`A previous upgrade left a stale backup at ${backupPath}; overwriting it.`);
|
|
350
|
+
removeFileBestEffort(backupPath);
|
|
351
351
|
}
|
|
352
352
|
try {
|
|
353
353
|
if (IS_WINDOWS)
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
8
8
|
import { isBundleSlug } from "../../core/asset/asset-ref.js";
|
|
9
9
|
import { displayRef, parseQualifiedRefInput } from "../../core/asset/resolve-ref.js";
|
|
10
|
+
import { isWithin } from "../../core/common.js";
|
|
10
11
|
import { loadConfig } from "../../core/config/config.js";
|
|
11
12
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
12
13
|
import { defaultBundleForTarget } from "../../core/mutation-target.js";
|
|
@@ -224,18 +225,15 @@ function lstatIfExists(filePath) {
|
|
|
224
225
|
}
|
|
225
226
|
function assertNoDestinationSymlinkParent(root, destination) {
|
|
226
227
|
const resolvedRoot = path.resolve(root);
|
|
227
|
-
const
|
|
228
|
+
const parentDir = path.dirname(path.resolve(destination));
|
|
229
|
+
const relativeParent = path.relative(resolvedRoot, parentDir);
|
|
228
230
|
if (relativeParent === "" || relativeParent === ".")
|
|
229
231
|
return;
|
|
230
232
|
if (relativeParent.startsWith("..") || path.isAbsolute(relativeParent)) {
|
|
231
233
|
throw new UsageError(`Clone destination escapes the selected target: ${destination}.`, "PATH_ESCAPE_VIOLATION");
|
|
232
234
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
current = path.join(current, segment);
|
|
236
|
-
if (lstatIfExists(current)?.isSymbolicLink()) {
|
|
237
|
-
throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${current}.`, "PATH_ESCAPE_VIOLATION");
|
|
238
|
-
}
|
|
235
|
+
if (!isWithin(parentDir, resolvedRoot)) {
|
|
236
|
+
throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${parentDir}.`, "PATH_ESCAPE_VIOLATION");
|
|
239
237
|
}
|
|
240
238
|
}
|
|
241
239
|
function listTreeFiles(root) {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { defineCommand } from "citty";
|
|
29
29
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
30
|
-
import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
30
|
+
import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
|
|
31
31
|
import { loadConfig } from "../../core/config/config.js";
|
|
32
32
|
import { UsageError } from "../../core/errors.js";
|
|
33
33
|
import { appendEvent } from "../../core/events.js";
|
|
@@ -54,12 +54,10 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
54
54
|
}
|
|
55
55
|
const skipPostUpgrade = args["skip-post-upgrade"];
|
|
56
56
|
const result = await performUpgrade(check, { force: args.force, skipPostUpgrade });
|
|
57
|
-
output("upgrade", result);
|
|
58
57
|
// The install may have succeeded, but an upgrade whose migration is
|
|
59
58
|
// blocked or could not run is not done: exit like `akm migrate apply` does.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
59
|
+
const migrationFailed = result.migration?.status === "blocked" || result.migration?.status === "failed";
|
|
60
|
+
outputWithExitCode("upgrade", result, migrationFailed ? EXIT_CODES.GENERAL : undefined);
|
|
63
61
|
},
|
|
64
62
|
});
|
|
65
63
|
// `sync` body, standalone so the git-commit/push logic stays in one place.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { defineCommand } from "citty";
|
|
28
28
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
29
29
|
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
30
|
-
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "../../cli/shared.js";
|
|
30
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
|
|
31
31
|
import { UsageError } from "../../core/errors.js";
|
|
32
32
|
import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
|
|
33
33
|
import { akmTaskExplain } from "./explain.js";
|
|
@@ -356,21 +356,16 @@ const tasksSyncCommand = defineJsonCommand({
|
|
|
356
356
|
const rebind = args.rebind === true;
|
|
357
357
|
if (args["dry-run"] === true) {
|
|
358
358
|
const preview = await akmTasksSyncPlan({}, args.bundle, { rebind });
|
|
359
|
-
|
|
360
|
-
const exitCode = taskSyncDryRunExitCode(preview);
|
|
361
|
-
if (exitCode !== undefined)
|
|
362
|
-
process.exitCode = exitCode;
|
|
359
|
+
outputWithExitCode("task-sync-dry-run", preview, taskSyncDryRunExitCode(preview));
|
|
363
360
|
return;
|
|
364
361
|
}
|
|
365
362
|
const result = await akmTasksSync({}, args.bundle, { rebind });
|
|
366
|
-
output("task-sync", result);
|
|
367
363
|
// #867: sync degrades — sources that failed to parse/prepare are
|
|
368
364
|
// excluded from reconciliation and reported in `result.failures` rather
|
|
369
365
|
// than poisoning the whole sync, but their presence must still fail
|
|
370
366
|
// the command's exit code so the breakage stays visible. (#906: this key
|
|
371
367
|
// matches the `--dry-run` preview's `failures` field — no separate name.)
|
|
372
|
-
|
|
373
|
-
process.exitCode = EXIT_CODES.GENERAL;
|
|
368
|
+
outputWithExitCode("task-sync", result, result.failures.length > 0 ? EXIT_CODES.GENERAL : undefined);
|
|
374
369
|
},
|
|
375
370
|
});
|
|
376
371
|
// ── `akm task explain` — read-only introspection (P2b Lane B, spec
|
|
@@ -482,10 +477,7 @@ const tasksPruneCommand = defineJsonCommand({
|
|
|
482
477
|
.filter(Boolean)
|
|
483
478
|
: undefined;
|
|
484
479
|
const result = await akmTasksPrune({}, { yes: args.yes === true, id });
|
|
485
|
-
|
|
486
|
-
const exitCode = taskPruneExitCode(result);
|
|
487
|
-
if (exitCode !== undefined)
|
|
488
|
-
process.exitCode = exitCode;
|
|
480
|
+
outputWithExitCode("task-prune", result, taskPruneExitCode(result));
|
|
489
481
|
},
|
|
490
482
|
});
|
|
491
483
|
export const taskCommand = defineGroupCommand({
|
|
@@ -22,6 +22,7 @@ import { resolveConfiguredSources } from "../../core/config/config-sources.js";
|
|
|
22
22
|
import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
|
|
23
23
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
24
24
|
import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
|
|
25
|
+
import { warn } from "../../core/warn.js";
|
|
25
26
|
import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
26
27
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
27
28
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
@@ -391,7 +392,7 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
391
392
|
const expectedSignature = sched.expectedSignature?.bind(sched);
|
|
392
393
|
const needsRuntime = preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined);
|
|
393
394
|
const prepared = needsRuntime
|
|
394
|
-
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps,
|
|
395
|
+
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, warnings, allEntries.map((entry) => entry.binding))
|
|
395
396
|
: undefined;
|
|
396
397
|
const plan = finalizeSchedulerSyncPlan({
|
|
397
398
|
...common,
|
|
@@ -768,22 +769,24 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
768
769
|
contextPath: installedEntry.contextPath,
|
|
769
770
|
},
|
|
770
771
|
}
|
|
771
|
-
: prepareSchedulerSyncRuntime(input.installOpts, input.deps,
|
|
772
|
+
: prepareSchedulerSyncRuntime(input.installOpts, input.deps, []);
|
|
772
773
|
const runtimeOpts = preparedRuntime.options;
|
|
773
774
|
const removals = taskEntries.map((entry) => {
|
|
774
775
|
const invocation = entry.invocation;
|
|
775
|
-
if (!invocation) {
|
|
776
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact parsed owner; refusing replacement.`, "RESOURCE_ALREADY_EXISTS");
|
|
777
|
-
}
|
|
778
776
|
const nativeId = entry.nativeId ?? schedulerNativeBindingId(entry.id);
|
|
779
777
|
const artifact = assertSchedulerNativeArtifactCardinality(nativeArtifacts, nativeId, 1);
|
|
780
778
|
if (!artifact?.fingerprint || artifact.bindingId !== entry.id) {
|
|
781
779
|
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact coherent fingerprint.`, "RESOURCE_ALREADY_EXISTS");
|
|
782
780
|
}
|
|
783
781
|
const logicalSource = primary.logicalSource;
|
|
784
|
-
const ordinal = schedulerBindingOrdinal(entry.id, logicalSource, invocation);
|
|
785
|
-
if (ordinal === undefined) {
|
|
786
|
-
|
|
782
|
+
const ordinal = invocation ? schedulerBindingOrdinal(entry.id, logicalSource, invocation) : undefined;
|
|
783
|
+
if (!invocation || ordinal === undefined) {
|
|
784
|
+
warn(`Installed scheduler binding ${JSON.stringify(entry.id)} (native id ${JSON.stringify(nativeId)}) could not be exactly parsed — likely a hand-edited entry; replacing it without a compare-and-swap guard.`);
|
|
785
|
+
return Object.freeze({
|
|
786
|
+
kind: "remove",
|
|
787
|
+
id: entry.id,
|
|
788
|
+
nativeId,
|
|
789
|
+
});
|
|
787
790
|
}
|
|
788
791
|
return Object.freeze({
|
|
789
792
|
kind: "remove",
|
|
@@ -824,6 +827,8 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
824
827
|
for (const removal of removals) {
|
|
825
828
|
if (removal.kind !== "remove")
|
|
826
829
|
continue;
|
|
830
|
+
if (!removal.expected)
|
|
831
|
+
continue;
|
|
827
832
|
initialByKey.set(schedulerNativeArtifactKey(removal.nativeId), Object.freeze({ ...removal.expected, state: "present" }));
|
|
828
833
|
}
|
|
829
834
|
for (const install of installs) {
|
|
@@ -841,16 +846,16 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
841
846
|
publishOperationIndex: removals.length,
|
|
842
847
|
});
|
|
843
848
|
}
|
|
844
|
-
function prepareSchedulerSyncRuntime(base, deps,
|
|
849
|
+
function prepareSchedulerSyncRuntime(base, deps, warnings, installedBindings = []) {
|
|
845
850
|
if (deps.backend && !deps.schedulerRuntime)
|
|
846
851
|
return base ? { options: base } : {};
|
|
847
852
|
if (deps.schedulerRuntime) {
|
|
848
853
|
const runtime = deps.schedulerRuntime();
|
|
849
|
-
warnIneligibleRebind(runtime,
|
|
854
|
+
warnIneligibleRebind(runtime, warnings, installedBindings);
|
|
850
855
|
return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
|
|
851
856
|
}
|
|
852
|
-
const invocation = resolveAndValidateSchedulerInvocation(
|
|
853
|
-
warnIneligibleRebind(invocation,
|
|
857
|
+
const invocation = resolveAndValidateSchedulerInvocation();
|
|
858
|
+
warnIneligibleRebind(invocation, warnings, installedBindings);
|
|
854
859
|
const descriptor = schedulerContextDescriptor();
|
|
855
860
|
const contextPath = schedulerContextPath(descriptor);
|
|
856
861
|
return {
|
|
@@ -863,24 +868,21 @@ function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warn
|
|
|
863
868
|
},
|
|
864
869
|
};
|
|
865
870
|
}
|
|
866
|
-
function resolveAndValidateSchedulerInvocation(
|
|
871
|
+
function resolveAndValidateSchedulerInvocation() {
|
|
867
872
|
const invocation = resolveAkmInvocation();
|
|
868
|
-
if (!invocation.eligible && !explicitRebind) {
|
|
869
|
-
throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
|
|
870
|
-
}
|
|
871
873
|
return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
|
|
872
874
|
}
|
|
873
|
-
function warnIneligibleRebind(runtime,
|
|
874
|
-
if (
|
|
875
|
+
function warnIneligibleRebind(runtime, warnings, installedBindings) {
|
|
876
|
+
if (runtime.eligible !== false || warnings.length > 0)
|
|
875
877
|
return;
|
|
876
|
-
// #868 residue:
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
878
|
+
// #868 residue: binding every currently-installed entry to the SAME
|
|
879
|
+
// invocation it already carries changes nothing — this is the steady
|
|
880
|
+
// state of an image-baked install re-running `task sync` on a timer.
|
|
881
|
+
// Only warn when the bind actually moves an entry to a different
|
|
882
|
+
// invocation.
|
|
881
883
|
if (installedBindings.length > 0 && installedBindings.every((bound) => sameArgv(bound, runtime.binding)))
|
|
882
884
|
return;
|
|
883
|
-
warnings.push(
|
|
885
|
+
warnings.push(`Scheduled tasks are bound to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
|
|
884
886
|
}
|
|
885
887
|
function groupInstalledBindings(entries, invocation) {
|
|
886
888
|
const groups = new Map();
|
|
@@ -972,16 +974,20 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
972
974
|
const common = { filePath, rootRealPath };
|
|
973
975
|
let descriptor;
|
|
974
976
|
try {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
977
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
978
|
+
let before = fs.fstatSync(descriptor, { bigint: true });
|
|
978
979
|
if (!before.isFile()) {
|
|
979
980
|
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
980
981
|
}
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
982
|
+
let bytes = fs.readFileSync(descriptor);
|
|
983
|
+
const torn = !sameTaskSourceStat(before, fs.fstatSync(descriptor, { bigint: true })) ||
|
|
984
|
+
BigInt(bytes.byteLength) !== before.size;
|
|
985
|
+
if (torn) {
|
|
986
|
+
fs.closeSync(descriptor);
|
|
987
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
988
|
+
before = fs.fstatSync(descriptor, { bigint: true });
|
|
989
|
+
bytes = fs.readFileSync(descriptor);
|
|
990
|
+
warn(`${filePath} changed while its guarded bytes were read; retried once and proceeding with the latest read (its SHA-256 is re-verified before anything is published).`);
|
|
985
991
|
}
|
|
986
992
|
const realPath = fs.realpathSync(filePath);
|
|
987
993
|
const physicalRelative = path.relative(rootRealPath, realPath);
|
|
@@ -1011,9 +1017,6 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
1011
1017
|
}
|
|
1012
1018
|
if (cause instanceof UsageError)
|
|
1013
1019
|
throw cause;
|
|
1014
|
-
if (cause.code === "ELOOP") {
|
|
1015
|
-
throw new UsageError(`${filePath} must not be a symbolic task source.`, "RESOURCE_ALREADY_EXISTS");
|
|
1016
|
-
}
|
|
1017
1020
|
throw new UsageError(`${filePath} could not be guarded as a contained regular task source: ${errorMessage(cause)}`, "PATH_ESCAPE_VIOLATION");
|
|
1018
1021
|
}
|
|
1019
1022
|
finally {
|
|
@@ -1264,7 +1267,7 @@ function assertInlineTaskPrompt(input) {
|
|
|
1264
1267
|
(!/\s/.test(value) && /[\\/]/.test(value) && path.extname(value) !== "");
|
|
1265
1268
|
if (!isFullRefInput(value) && !pathShaped)
|
|
1266
1269
|
return;
|
|
1267
|
-
|
|
1270
|
+
warn(`--prompt "${input}" looks like an asset ref or file path; --prompt sends it as literal text, not a reference. Did you mean --workflow?`);
|
|
1268
1271
|
}
|
|
1269
1272
|
function parseJsonObjectArg(raw) {
|
|
1270
1273
|
let parsed;
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
* GitHub-shaped `.yml` workflow sources. Validate with `akm lint --type workflows`.
|
|
9
9
|
*/
|
|
10
10
|
import { getStringArg } from "../cli/parse-args.js";
|
|
11
|
-
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
11
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output, outputWithExitCode } from "../cli/shared.js";
|
|
12
12
|
import { armAbortDeadline } from "../core/abort-deadline.js";
|
|
13
13
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
|
|
14
14
|
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
15
15
|
import { akmIndex } from "../indexer/indexer.js";
|
|
16
16
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
17
17
|
import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
18
|
-
import { abandonWorkflowRun, getWorkflowStatus,
|
|
18
|
+
import { abandonWorkflowRun, getWorkflowStatus, listWorkflowRuns, resolveWorkflowRunTarget, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
19
19
|
import { akmWorkflowPlan } from "./workflow/plan.js";
|
|
20
20
|
const workflowStatusCommand = defineJsonCommand({
|
|
21
21
|
meta: {
|
|
@@ -34,8 +34,9 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
34
34
|
async run({ args }) {
|
|
35
35
|
const target = args.target;
|
|
36
36
|
const includeUnits = args.units === true;
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const resolvedRunId = await resolveWorkflowRunTarget(target);
|
|
38
|
+
if (resolvedRunId !== undefined) {
|
|
39
|
+
const result = await getWorkflowStatus(resolvedRunId, { includeUnits });
|
|
39
40
|
output("workflow-status", result);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
@@ -100,12 +101,12 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
100
101
|
},
|
|
101
102
|
force: {
|
|
102
103
|
type: "boolean",
|
|
103
|
-
description: "Overwrite an existing workflow
|
|
104
|
+
description: "Overwrite an existing workflow. Combined with --from, replaces its content; alone, replaces it with a fresh template.",
|
|
104
105
|
default: false,
|
|
105
106
|
},
|
|
106
107
|
reset: {
|
|
107
108
|
type: "boolean",
|
|
108
|
-
description: "
|
|
109
|
+
description: "Deprecated alias for --force with no --from (replaces an existing workflow with a fresh template).",
|
|
109
110
|
default: false,
|
|
110
111
|
},
|
|
111
112
|
print: {
|
|
@@ -130,9 +131,6 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
130
131
|
process.stdout.write(getWorkflowTemplate());
|
|
131
132
|
return;
|
|
132
133
|
}
|
|
133
|
-
if (args.force && !args.from && !args.reset) {
|
|
134
|
-
throw new UsageError("Refusing to overwrite with template: pass --from <file> to replace content, or --reset to explicitly replace with a fresh template.");
|
|
135
|
-
}
|
|
136
134
|
const result = createWorkflowAsset({
|
|
137
135
|
name: effectiveName,
|
|
138
136
|
from: args.from,
|
|
@@ -157,6 +155,12 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
157
155
|
"max-steps": { type: "string", description: "Stop after executing this many steps" },
|
|
158
156
|
"max-retries": { type: "string", description: "Retry a failed workflow step this many additional times" },
|
|
159
157
|
timeout: { type: "string", description: "Whole-run timeout: N, Nms, Ns, or Nm (bare N is milliseconds)" },
|
|
158
|
+
new: {
|
|
159
|
+
type: "boolean",
|
|
160
|
+
description: "Start a fresh run even if one is already active for this ref, leaving the existing run untouched " +
|
|
161
|
+
"(never abandons it). A workflow ref only — passing a run id with --new is a usage error.",
|
|
162
|
+
default: false,
|
|
163
|
+
},
|
|
160
164
|
},
|
|
161
165
|
async run({ args, rawArgs }) {
|
|
162
166
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
@@ -186,6 +190,7 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
186
190
|
parameterFlags,
|
|
187
191
|
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
188
192
|
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
193
|
+
newRun: args.new,
|
|
189
194
|
signal: controller.signal,
|
|
190
195
|
});
|
|
191
196
|
// The abort is observed between steps, so a deadline landing in the run's
|
|
@@ -193,14 +198,11 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
193
198
|
// timed out would send an operator to resume a run with nothing left to
|
|
194
199
|
// resume — `tasks/runner.ts` suppresses the same case.
|
|
195
200
|
const timedOut = deadline.timedOut() && result.run.status !== "completed";
|
|
196
|
-
const rendered = { ...result, ...(timedOut ? { timedOut: true } : {}) };
|
|
197
|
-
output("workflow-run", rendered);
|
|
198
201
|
// `blocked` is a stopped, unverified run — a verification-judge failure
|
|
199
202
|
// leaves it there for `akm workflow resume` — so it must not exit 0 and
|
|
200
203
|
// read as success to a script (it maps to 1 for scheduled tasks too).
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
+
const failed = result.run.status === "failed" || result.run.status === "blocked" || result.gateRejection || result.aborted;
|
|
205
|
+
outputWithExitCode("workflow-run", { ...result, ...(timedOut ? { timedOut: true } : {}) }, failed ? (signalExitCode ?? EXIT_CODES.GENERAL) : undefined);
|
|
204
206
|
}
|
|
205
207
|
finally {
|
|
206
208
|
deadline.disarm();
|
|
@@ -220,7 +222,7 @@ const WORKFLOW_RUN_VALUE_FLAGS = new Set([
|
|
|
220
222
|
"shape",
|
|
221
223
|
"output",
|
|
222
224
|
]);
|
|
223
|
-
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose"]);
|
|
225
|
+
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose", "new", "no-new"]);
|
|
224
226
|
export function parseWorkflowParameterFlags(rawArgs, target) {
|
|
225
227
|
const flags = [];
|
|
226
228
|
let targetSeen = false;
|
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `EDITOR`/`VISUAL`/`PAGER` are flagged by `isDangerousEnvKey` for a
|
|
6
|
+
* documented RCE vector (many tools invoke them to launch an editor/pager),
|
|
7
|
+
* but env-key-rules.ts's own module doc calls out their "high FP rate" in
|
|
8
|
+
* the same breath — an installed bundle's env file can supply a value, but
|
|
9
|
+
* nothing in akm's own env-injection path ever *invokes* EDITOR/VISUAL/PAGER
|
|
10
|
+
* with that value, so the RCE vector these three describe cannot fire from
|
|
11
|
+
* an injected env the way LD_PRELOAD or GIT_SSH_COMMAND can. Blocking a
|
|
12
|
+
* third-party install over them protects nothing while making "the operator
|
|
13
|
+
* legitimately wants to set their editor" the common case that eats the
|
|
14
|
+
* refusal. Kept as a name-level literal set (not an import of
|
|
15
|
+
* `commands/lint/env-key-rules.ts`) so this module stays the pure leaf its
|
|
16
|
+
* own doc comment promises — no new import edges into the rest of the tree.
|
|
17
|
+
*/
|
|
18
|
+
const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
|
|
4
19
|
/**
|
|
5
20
|
* Decide whether injecting an env with the given dangerous keys is allowed,
|
|
6
|
-
* warned, or blocked. Third-party (registry-installed) stashes hard-block
|
|
7
|
-
* first-party stashes warn.
|
|
21
|
+
* warned, or blocked. Third-party (registry-installed) stashes hard-block a
|
|
22
|
+
* genuine RCE-class key; first-party stashes warn. The interactive-tool
|
|
23
|
+
* group (see {@link INTERACTIVE_TOOL_ENV_KEYS}) only ever warns, since akm's
|
|
24
|
+
* own env-injection path never invokes those keys as a command. An explicit
|
|
25
|
+
* `--allow-insecure` (threaded through by the caller, same override
|
|
26
|
+
* `decideDangerousKeyInstall`'s `"warn-allow"` already honors for rule 2)
|
|
27
|
+
* downgrades a remaining block to a warning too — the operator is not racing
|
|
28
|
+
* themselves. See rule 1 above.
|
|
8
29
|
*
|
|
9
30
|
* @param dangerousKeys The subset of injected keys flagged as process-hijacking
|
|
10
31
|
* (already filtered by the caller via `isDangerousEnvKey`).
|
|
11
32
|
* @param thirdParty `true` when the env's source is a third-party stash — i.e.
|
|
12
33
|
* its origin carries a `registryId`.
|
|
34
|
+
* @param allowInsecure `true` when the operator passed `--allow-insecure` (or
|
|
35
|
+
* its equivalent) for this injection. Defaults to `false`.
|
|
13
36
|
*/
|
|
14
37
|
export function decideDangerousEnvInjection(input) {
|
|
15
38
|
if (input.dangerousKeys.length === 0)
|
|
16
39
|
return "allow";
|
|
17
|
-
|
|
40
|
+
if (!input.thirdParty)
|
|
41
|
+
return "warn";
|
|
42
|
+
if (input.allowInsecure)
|
|
43
|
+
return "warn";
|
|
44
|
+
const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
|
|
45
|
+
return onlyInteractiveTool ? "warn" : "block";
|
|
18
46
|
}
|
|
19
47
|
/**
|
|
20
48
|
* Decide the baseline install stance for a freshly-installed stash's
|
|
@@ -6,6 +6,8 @@ import { isMap, isScalar, parseDocument, visit } from "yaml";
|
|
|
6
6
|
import { cloneExecutionJson, cloneExecutionJsonObject, } from "../../execution/json.js";
|
|
7
7
|
import { assertSnapshotKeys, snapshotStrictRecord } from "../../execution/record.js";
|
|
8
8
|
import { cloneToolSelection, createAdapterRenderedExecutionSource, } from "../../execution/source.js";
|
|
9
|
+
import { UsageError } from "../errors.js";
|
|
10
|
+
import { warnOnce } from "../warn.js";
|
|
9
11
|
function nextLine(text, start) {
|
|
10
12
|
const lf = text.indexOf("\n", start);
|
|
11
13
|
if (lf < 0)
|
|
@@ -13,16 +15,39 @@ function nextLine(text, start) {
|
|
|
13
15
|
const end = lf > start && text[lf - 1] === "\r" ? lf - 1 : lf;
|
|
14
16
|
return { line: text.slice(start, end), next: lf + 1 };
|
|
15
17
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Strict execution-only frontmatter parser; indexing's tolerant parser is
|
|
20
|
+
* deliberately not reused.
|
|
21
|
+
*
|
|
22
|
+
* `filePath`, when given, names the offending asset in a thrown error so an
|
|
23
|
+
* operator (or CI) sees which file to fix instead of a bare message.
|
|
24
|
+
*
|
|
25
|
+
* A third party's markdown that genuinely cannot be parsed into a mapping
|
|
26
|
+
* (unterminated frontmatter, broken YAML syntax, a non-mapping document)
|
|
27
|
+
* throws {@link UsageError} \u2014 exit 2, an actionable usage problem, not exit
|
|
28
|
+
* 70 (`TypeError`'s "unclassified internal error" code, which is what a
|
|
29
|
+
* `throw new TypeError(...)` here used to surface as). Anchors, explicit
|
|
30
|
+
* tags, and non-string mapping keys are recognized-but-unsupported
|
|
31
|
+
* constructs rather than parse failures on their own \u2014 an anchor nobody
|
|
32
|
+
* aliases, or a tag `toJS` converts to a plain value, is harmless \u2014 so they
|
|
33
|
+
* warn once (naming the file) and fall through to conversion instead of
|
|
34
|
+
* being pre-emptively rejected by a second, stricter gate in front of it.
|
|
35
|
+
* `toJS({ maxAliasCount: 0 })` is the real, UNCHANGED safety bound against
|
|
36
|
+
* alias-expansion abuse \u2014 it disables alias RESOLUTION outright, so a value
|
|
37
|
+
* that actually references an anchor via `*name` still fails, now as a
|
|
38
|
+
* {@link UsageError} from this same conversion step rather than the removed
|
|
39
|
+
* duplicate pre-check.
|
|
40
|
+
*/
|
|
41
|
+
export function parseExecutionMarkdown(raw, filePath) {
|
|
18
42
|
if (typeof raw !== "string")
|
|
19
43
|
throw new TypeError("execution source raw content must be a string");
|
|
44
|
+
const where = filePath ? ` (${filePath})` : "";
|
|
20
45
|
const withoutBom = raw.startsWith("\uFEFF") ? raw.slice(1) : raw;
|
|
21
46
|
const opening = nextLine(withoutBom, 0);
|
|
22
47
|
if (opening.line !== "---")
|
|
23
48
|
return { content: withoutBom, data: Object.freeze({}) };
|
|
24
49
|
if (opening.next === withoutBom.length) {
|
|
25
|
-
throw new
|
|
50
|
+
throw new UsageError(`execution source${where} has unterminated frontmatter`);
|
|
26
51
|
}
|
|
27
52
|
let cursor = opening.next;
|
|
28
53
|
let frontmatterEnd = -1;
|
|
@@ -40,18 +65,18 @@ export function parseExecutionMarkdown(raw) {
|
|
|
40
65
|
cursor = current.next;
|
|
41
66
|
}
|
|
42
67
|
if (frontmatterEnd < 0 || bodyStart < 0) {
|
|
43
|
-
throw new
|
|
68
|
+
throw new UsageError(`execution source${where} has unterminated frontmatter`);
|
|
44
69
|
}
|
|
45
70
|
const yaml = withoutBom.slice(opening.next, frontmatterEnd);
|
|
46
71
|
const document = parseDocument(yaml, { uniqueKeys: true });
|
|
47
72
|
if (document.errors.length > 0) {
|
|
48
|
-
throw new
|
|
73
|
+
throw new UsageError(`execution source${where} has invalid YAML frontmatter: ${document.errors[0]?.message ?? "parse error"}`);
|
|
49
74
|
}
|
|
50
75
|
if (document.warnings.length > 0) {
|
|
51
|
-
|
|
76
|
+
warnOnce(`execution-source:yaml-warning:${filePath ?? "<inline>"}`, `execution source${where} YAML frontmatter uses an unsupported tag or construct: ${document.warnings[0]?.message}. Using it as parsed.`);
|
|
52
77
|
}
|
|
53
78
|
if (!isMap(document.contents)) {
|
|
54
|
-
throw new
|
|
79
|
+
throw new UsageError(`execution source${where} YAML frontmatter must be a mapping`);
|
|
55
80
|
}
|
|
56
81
|
let unsupported;
|
|
57
82
|
visit(document, {
|
|
@@ -69,14 +94,16 @@ export function parseExecutionMarkdown(raw) {
|
|
|
69
94
|
unsupported ??= "non-string mapping keys";
|
|
70
95
|
},
|
|
71
96
|
});
|
|
72
|
-
if (unsupported)
|
|
73
|
-
|
|
97
|
+
if (unsupported) {
|
|
98
|
+
warnOnce(`execution-source:unsupported:${filePath ?? "<inline>"}`, `execution source${where} YAML frontmatter uses ${unsupported}, which akm does not fully support; using the bounded conversion's result as-is.`);
|
|
99
|
+
}
|
|
74
100
|
let root;
|
|
75
101
|
try {
|
|
76
102
|
root = document.toJS({ maxAliasCount: 0 });
|
|
77
103
|
}
|
|
78
104
|
catch (cause) {
|
|
79
|
-
|
|
105
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
106
|
+
throw new UsageError(`execution source${where} YAML frontmatter could not be converted safely: ${detail}`);
|
|
80
107
|
}
|
|
81
108
|
const data = cloneExecutionJsonObject(root, "execution source YAML frontmatter");
|
|
82
109
|
return { content: withoutBom.slice(bodyStart), data: Object.freeze(data) };
|
|
@@ -279,7 +306,8 @@ export function renderMarkdownExecutionSource(input) {
|
|
|
279
306
|
const kind = snapshots.input.kind;
|
|
280
307
|
if (kind !== "command" && kind !== "persona")
|
|
281
308
|
throw new TypeError("adapter execution source.kind is invalid");
|
|
282
|
-
const
|
|
309
|
+
const identityFile = snapshots.identity.file;
|
|
310
|
+
const parsed = parseExecutionMarkdown(raw, typeof identityFile === "string" ? identityFile : undefined);
|
|
283
311
|
const hasDefaults = Object.hasOwn(snapshots.input, "defaults");
|
|
284
312
|
const defaultsProjection = snapshots.input.defaults;
|
|
285
313
|
const defaults = typeof defaultsProjection === "function" ? defaultsProjection(parsed.data) : defaultsProjection;
|