akm-cli 0.9.0 → 0.9.1-beta.2
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 +724 -0
- package/README.md +28 -63
- package/STABILITY.md +4 -2
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/feedback-cli.js +7 -1
- package/dist/commands/health/llm-usage.js +2 -1
- package/dist/commands/health/surfaces.js +4 -77
- package/dist/commands/health.js +65 -11
- package/dist/commands/improve/distill/quality-gate.js +6 -1
- package/dist/commands/improve/eligibility.js +7 -1
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/improve.js +126 -10
- package/dist/commands/improve/locks.js +7 -0
- package/dist/commands/improve/memory/memory-improve.js +9 -0
- package/dist/commands/improve/run-context.js +5 -0
- package/dist/commands/improve/session-asset.js +4 -0
- package/dist/commands/lint/base-linter.js +31 -7
- package/dist/commands/lint/index.js +205 -51
- package/dist/commands/lint/types.js +22 -1
- package/dist/commands/proposal/repository.js +17 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/info.js +12 -2
- package/dist/commands/sources/installed-stashes.js +6 -1
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/commands/tasks/tasks.js +8 -2
- package/dist/commands/workflow-cli.js +17 -11
- package/dist/core/abort-deadline.js +28 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
- package/dist/core/adapter/adapters/akm-adapter.js +13 -10
- package/dist/core/adapter/adapters/akm-lint.js +78 -22
- package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/common.js +147 -9
- package/dist/core/concurrent.js +32 -0
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/config/schema/workflow.js +11 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/events.js +30 -24
- package/dist/core/extra-params.js +11 -0
- package/dist/core/file-lock.js +7 -1
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/improve-result.js +5 -0
- package/dist/core/json-schema.js +344 -9
- package/dist/core/loopback.js +89 -0
- package/dist/core/migration-operation.js +17 -2
- package/dist/core/path-access.js +107 -0
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +86 -18
- package/dist/core/spawn-env.js +234 -0
- package/dist/core/state-db-scope.js +134 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/subprocess.js +181 -37
- package/dist/core/write-provenance.js +85 -0
- package/dist/core/write-source.js +33 -2
- package/dist/indexer/db/graph-db.js +17 -6
- package/dist/indexer/ensure-index.js +10 -3
- package/dist/indexer/index-written-assets.js +17 -2
- package/dist/indexer/indexer.js +86 -21
- package/dist/indexer/passes/memory-inference.js +4 -0
- package/dist/indexer/search/db-search.js +25 -17
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/agent/engine-resolution.js +24 -11
- package/dist/integrations/agent/model-aliases.js +1 -1
- package/dist/integrations/agent/profiles.js +9 -1
- package/dist/integrations/agent/spawn.js +15 -87
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +55 -2
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/output/text/lint-format.js +17 -4
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +1714 -836
- package/dist/scripts/akm-migrate.js +1682 -804
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/providers/git-stash.js +19 -0
- package/dist/sources/providers/git.js +1 -1
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
- package/dist/storage/database.js +71 -7
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/managed-db.js +19 -0
- package/dist/storage/repositories/index-connection.js +39 -4
- package/dist/storage/repositories/index-entries-repository.js +6 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/repositories/workflow-runs-repository.js +66 -13
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/log-redaction.js +156 -0
- package/dist/tasks/parser.js +82 -5
- package/dist/tasks/runner.js +222 -17
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +86 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/concurrency-policy.js +95 -1
- package/dist/workflows/exec/dispatch-redaction.js +114 -0
- package/dist/workflows/exec/exec-unit.js +542 -0
- package/dist/workflows/exec/frozen-judge.js +114 -42
- package/dist/workflows/exec/native-executor.js +465 -238
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +424 -219
- package/dist/workflows/exec/step-work.js +506 -167
- package/dist/workflows/exec/unit-dispatch.js +31 -1
- package/dist/workflows/exec/unit-writer.js +53 -13
- package/dist/workflows/exec/worktree.js +454 -41
- package/dist/workflows/ir/compile.js +26 -2
- package/dist/workflows/ir/freeze.js +82 -15
- package/dist/workflows/ir/schema.js +105 -20
- package/dist/workflows/parser.js +242 -19
- package/dist/workflows/program/schema.js +24 -0
- package/dist/workflows/renderer.js +32 -4
- package/dist/workflows/resource-limits.js +182 -0
- package/dist/workflows/runtime/runs.js +146 -6
- package/dist/workflows/validate-summary.js +17 -2
- package/docs/README.md +74 -32
- package/docs/migration/release-notes/0.9.0.md +2 -1
- package/docs/migration/v0.7-to-v0.8.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +11 -4
- package/docs/reference/bundle-types.md +19 -0
- package/docs/reference/cli.md +105 -16
- package/docs/reference/configuration.md +15 -2
- package/docs/reference/data-and-telemetry.md +30 -10
- package/docs/reference/supported-formats.md +50 -0
- package/docs/reference/workflow-schema.md +1014 -0
- package/docs/reference/workflows.md +37 -633
- package/package.json +13 -6
- package/schemas/akm-config.json +18 -5
- package/schemas/akm-task.json +27 -5
- package/schemas/akm-workflow.json +92 -13
|
@@ -14,7 +14,8 @@ import { resolveMutationTarget } from "../../core/mutation-target.js";
|
|
|
14
14
|
import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
|
|
15
15
|
import { redactSensitiveText } from "../../core/redaction.js";
|
|
16
16
|
import { openStateDatabase } from "../../core/state-db.js";
|
|
17
|
-
import { info, warn } from "../../core/warn.js";
|
|
17
|
+
import { info, warn, warnVerbose } from "../../core/warn.js";
|
|
18
|
+
import { beginWriteProvenance, relativeWrittenPath } from "../../core/write-provenance.js";
|
|
18
19
|
import { resolveWritable, resolveWriteTarget } from "../../core/write-source.js";
|
|
19
20
|
import { ensureIndex } from "../../indexer/ensure-index.js";
|
|
20
21
|
import { akmIndex } from "../../indexer/indexer.js";
|
|
@@ -84,12 +85,38 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The run's write-provenance journal lifecycle as one named unit (#652).
|
|
90
|
+
*
|
|
91
|
+
* The journal is opened once the run owns its lock — so it spans exactly the
|
|
92
|
+
* window in which this run, and only this run, is allowed to write — and closed
|
|
93
|
+
* on every exit path including the crash-safety commit. Keeping the mutable
|
|
94
|
+
* handle behind this factory rather than as a bare `let` in {@link akmImprove}
|
|
95
|
+
* is also what keeps that function under the R31 size gate
|
|
96
|
+
* (`tests/architecture/improve-fn-size-ratchet.test.ts`, absolute 220-line bar
|
|
97
|
+
* with an empty baseline): lifecycle state belongs in a named unit, not in the
|
|
98
|
+
* orchestrator's preamble.
|
|
99
|
+
*/
|
|
100
|
+
function createRunWriteJournal() {
|
|
101
|
+
let journal;
|
|
102
|
+
return {
|
|
103
|
+
open: () => {
|
|
104
|
+
journal = beginWriteProvenance();
|
|
105
|
+
},
|
|
106
|
+
close: () => {
|
|
107
|
+
journal?.end();
|
|
108
|
+
journal = undefined;
|
|
109
|
+
},
|
|
110
|
+
current: () => journal,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
87
113
|
export async function akmImprove(options = {}) {
|
|
88
114
|
const setup = resolveImproveRunSetup(options);
|
|
89
115
|
options = setup.options;
|
|
90
116
|
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, lockStaleAfterMs, } = setup;
|
|
91
117
|
let clearBudgetTimer = () => { };
|
|
92
118
|
let initialGitPaths = new Set();
|
|
119
|
+
const runJournal = createRunWriteJournal();
|
|
93
120
|
const preEnsureCleanupWarnings = [];
|
|
94
121
|
// Assigned by runIndexAndCollect() (closure) so TS cannot prove definite
|
|
95
122
|
// assignment — seed with empty values; the runIndexAndCollect() call below
|
|
@@ -126,6 +153,7 @@ export async function akmImprove(options = {}) {
|
|
|
126
153
|
const commitStashBatch = makeCommitStashBatch({
|
|
127
154
|
run: setup,
|
|
128
155
|
getInitialGitPaths: () => initialGitPaths,
|
|
156
|
+
getWriteJournal: () => runJournal.current(),
|
|
129
157
|
// Captured via getter: the live `eventsCtx` binding is reassigned after the
|
|
130
158
|
// prepass, and the catch-path sync must write through the current context.
|
|
131
159
|
getEventsCtx: () => eventsCtx,
|
|
@@ -150,6 +178,9 @@ export async function akmImprove(options = {}) {
|
|
|
150
178
|
process.on("exit", exitBackstop);
|
|
151
179
|
initialGitPaths =
|
|
152
180
|
syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
|
|
181
|
+
// #652: open AFTER the lock, so the journal covers exactly the window in
|
|
182
|
+
// which this run — and only this run — is allowed to write.
|
|
183
|
+
runJournal.open();
|
|
153
184
|
// Drain the standing proposal backlog before indexing so fresh proposal
|
|
154
185
|
// generation sees promotions from this same serialized run.
|
|
155
186
|
triageDrain = await runTriagePrePass(setup);
|
|
@@ -180,6 +211,7 @@ export async function akmImprove(options = {}) {
|
|
|
180
211
|
exitBackstop = undefined;
|
|
181
212
|
}
|
|
182
213
|
releaseRunLock();
|
|
214
|
+
runJournal.close();
|
|
183
215
|
throw err;
|
|
184
216
|
}
|
|
185
217
|
try {
|
|
@@ -230,6 +262,13 @@ export async function akmImprove(options = {}) {
|
|
|
230
262
|
// the only commit; for a multi-cycle run the earlier cycles were banked by
|
|
231
263
|
// the inter-cycle calls and this records the last batch). `result` carries
|
|
232
264
|
// the full `{accepted}`/`{refs}`/`{triage_*}` token data for the message.
|
|
265
|
+
//
|
|
266
|
+
// #652: surface the run's write provenance on the envelope BEFORE the sync
|
|
267
|
+
// (the sync itself writes no assets), so `result.writtenPaths` describes
|
|
268
|
+
// exactly the set the commit below was scoped to.
|
|
269
|
+
const writtenPaths = describeRunWrittenPaths(setup, runJournal.current()?.writtenPaths() ?? []);
|
|
270
|
+
if (writtenPaths.length > 0)
|
|
271
|
+
result.writtenPaths = writtenPaths;
|
|
233
272
|
result.sync = commitStashBatch(result);
|
|
234
273
|
return result;
|
|
235
274
|
}
|
|
@@ -261,6 +300,10 @@ export async function akmImprove(options = {}) {
|
|
|
261
300
|
exitBackstop = undefined;
|
|
262
301
|
}
|
|
263
302
|
releaseRunLock();
|
|
303
|
+
// #652: close the write-provenance journal LAST among the write-facing
|
|
304
|
+
// teardown steps — the catch path's crash-safety commit above still needs
|
|
305
|
+
// it open to know what this run wrote.
|
|
306
|
+
runJournal.close();
|
|
264
307
|
// I1: close the long-lived state.db connection opened at the top of the run.
|
|
265
308
|
try {
|
|
266
309
|
eventsDb?.close();
|
|
@@ -270,6 +313,22 @@ export async function akmImprove(options = {}) {
|
|
|
270
313
|
}
|
|
271
314
|
}
|
|
272
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Shape the run's journaled absolute paths for `result.writtenPaths` (#652):
|
|
318
|
+
* POSIX-relative to the run's primary stash dir (the repo root for a git-backed
|
|
319
|
+
* stash) when the write landed inside it, absolute otherwise — a run writing to
|
|
320
|
+
* a `--target` bundle outside the stash still reports what it wrote. Deduped and
|
|
321
|
+
* sorted so the field is stable across runs.
|
|
322
|
+
*/
|
|
323
|
+
function describeRunWrittenPaths(setup, writtenPaths) {
|
|
324
|
+
const root = setup.primaryStashDir ?? setup.syncRepoDir;
|
|
325
|
+
const described = new Set();
|
|
326
|
+
for (const absolutePath of writtenPaths) {
|
|
327
|
+
const relative = root ? relativeWrittenPath(root, absolutePath) : undefined;
|
|
328
|
+
described.add(relative ?? absolutePath.replaceAll(path.sep, "/"));
|
|
329
|
+
}
|
|
330
|
+
return [...described].sort();
|
|
331
|
+
}
|
|
273
332
|
/** Resolve a dry-run inspection source without adapting it into a write target. */
|
|
274
333
|
function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
|
|
275
334
|
if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
|
|
@@ -659,19 +718,67 @@ async function runTriagePrePass(run) {
|
|
|
659
718
|
}
|
|
660
719
|
return triageDrain;
|
|
661
720
|
}
|
|
721
|
+
/** A path is never staged by auto-sync when it is (or looks like) a lock file. */
|
|
722
|
+
function isSyncExcludedPath(relativePath) {
|
|
723
|
+
return path.basename(relativePath).includes(".lock");
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Resolve the EXACT repo-relative path set the auto-sync commit stages (#652).
|
|
727
|
+
*
|
|
728
|
+
* Precedence:
|
|
729
|
+
*
|
|
730
|
+
* 1. **Run-scoped write provenance** (`writtenPaths`, absolute) — the paths
|
|
731
|
+
* this run actually wrote, created, or removed, intersected with the paths
|
|
732
|
+
* Git currently reports as changed. The intersection is what makes the set
|
|
733
|
+
* correct rather than merely plausible: it drops a journaled path whose
|
|
734
|
+
* final bytes match HEAD (nothing to commit), a journaled path that was
|
|
735
|
+
* created and then removed again (never existed for Git), and any journaled
|
|
736
|
+
* path Git ignores (which `saveGitStash` would otherwise reject outright).
|
|
737
|
+
* A journaled path that was ALREADY dirty when the run started stays IN —
|
|
738
|
+
* this run rewrote it, so this run owns it.
|
|
739
|
+
* 2. **Dirty-path diff** (pre-#652 behaviour) — only when no journal was open,
|
|
740
|
+
* which a live run never hits. Kept as the defensive fallback so a future
|
|
741
|
+
* caller that commits outside the journal window degrades to the previous,
|
|
742
|
+
* battle-tested heuristic instead of silently committing nothing.
|
|
743
|
+
*
|
|
744
|
+
* `unattributed` counts in-scope paths that became dirty DURING the run without
|
|
745
|
+
* this run writing them: concurrent human edits (correctly excluded) and, if the
|
|
746
|
+
* number is ever surprising, the fingerprint of a missing journal call site.
|
|
747
|
+
*/
|
|
748
|
+
export function resolveSyncPathSet(input) {
|
|
749
|
+
const { repoDir, assetPrefix, changedPaths, initialPaths, writtenPaths, provenance } = input;
|
|
750
|
+
const inScope = (relativePath) => !isSyncExcludedPath(relativePath) &&
|
|
751
|
+
(!assetPrefix || relativePath === assetPrefix || relativePath.startsWith(`${assetPrefix}/`));
|
|
752
|
+
if (!provenance) {
|
|
753
|
+
return { paths: changedPaths.filter((p) => !initialPaths.has(p) && inScope(p)), unattributed: [] };
|
|
754
|
+
}
|
|
755
|
+
const changed = new Set(changedPaths);
|
|
756
|
+
const attributed = new Set();
|
|
757
|
+
for (const absolutePath of writtenPaths) {
|
|
758
|
+
const relativePath = relativeWrittenPath(repoDir, absolutePath);
|
|
759
|
+
if (!relativePath || !changed.has(relativePath) || !inScope(relativePath))
|
|
760
|
+
continue;
|
|
761
|
+
attributed.add(relativePath);
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
paths: [...attributed].sort(),
|
|
765
|
+
unattributed: changedPaths.filter((p) => !attributed.has(p) && !initialPaths.has(p) && inScope(p)).sort(),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
662
768
|
/**
|
|
663
769
|
* Crash-safe / incremental stash sync (#662) — see the factory-returned
|
|
664
770
|
* closure's original doc block: the primary stash writes as a filesystem
|
|
665
771
|
* source DURING the run; this commits them at end-of-run AND from the catch
|
|
666
|
-
* path. Idempotent + NON-FATAL. `getEventsCtx`/`getInitialGitPaths
|
|
667
|
-
* getters because
|
|
668
|
-
* runs — the returned closure must observe the live values.
|
|
772
|
+
* path. Idempotent + NON-FATAL. `getEventsCtx`/`getInitialGitPaths`/
|
|
773
|
+
* `getWriteJournal` are getters because those outer bindings are (re)assigned
|
|
774
|
+
* after this factory runs — the returned closure must observe the live values.
|
|
669
775
|
*/
|
|
670
776
|
function makeCommitStashBatch(deps) {
|
|
671
777
|
const { writeTarget, primaryStashDir, effectiveSync, options, _earlyConfig, improveProfile } = deps.run;
|
|
672
778
|
return (messageContext) => {
|
|
673
779
|
const eventsCtx = deps.getEventsCtx();
|
|
674
780
|
const initialGitPaths = deps.getInitialGitPaths();
|
|
781
|
+
const writeJournal = deps.getWriteJournal();
|
|
675
782
|
const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
|
|
676
783
|
if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
|
|
677
784
|
return undefined;
|
|
@@ -683,13 +790,17 @@ function makeCommitStashBatch(deps) {
|
|
|
683
790
|
try {
|
|
684
791
|
const assetRoot = writeTarget?.source.path ?? primaryStashDir;
|
|
685
792
|
const assetPrefix = assetRoot ? path.relative(repoDir, assetRoot).replaceAll(path.sep, "/") : "";
|
|
686
|
-
const paths =
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
793
|
+
const { paths, unattributed } = resolveSyncPathSet({
|
|
794
|
+
repoDir,
|
|
795
|
+
assetPrefix,
|
|
796
|
+
changedPaths: listGitChangedPaths(repoDir),
|
|
797
|
+
initialPaths: initialGitPaths,
|
|
798
|
+
writtenPaths: writeJournal?.writtenPaths() ?? [],
|
|
799
|
+
provenance: writeJournal !== undefined,
|
|
692
800
|
});
|
|
801
|
+
if (unattributed.length > 0) {
|
|
802
|
+
warnVerbose(`[improve] auto-sync left ${unattributed.length} path(s) uncommitted — not written by this run: ${unattributed.join(", ")}`);
|
|
803
|
+
}
|
|
693
804
|
const syncResult = saveGitStashFn(undefined, message, writableOverride, {
|
|
694
805
|
push,
|
|
695
806
|
repoDir,
|
|
@@ -702,6 +813,11 @@ function makeCommitStashBatch(deps) {
|
|
|
702
813
|
pushed: syncResult.pushed,
|
|
703
814
|
skipped: syncResult.skipped,
|
|
704
815
|
reason: syncResult.reason ?? null,
|
|
816
|
+
// #652 provenance audit trail: how many paths this run staged, and
|
|
817
|
+
// how many in-scope paths went dirty during the run that it did NOT
|
|
818
|
+
// write (concurrent edits — deliberately left for their author).
|
|
819
|
+
attributed: paths.length,
|
|
820
|
+
unattributed: unattributed.length,
|
|
705
821
|
},
|
|
706
822
|
}, eventsCtx);
|
|
707
823
|
return {
|
|
@@ -7,6 +7,7 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
7
7
|
import { appendEvent } from "../../core/events.js";
|
|
8
8
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
|
|
9
9
|
import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
|
|
10
|
+
import { describeInaccessiblePath } from "../../core/path-access.js";
|
|
10
11
|
import { warn } from "../../core/warn.js";
|
|
11
12
|
export const MIN_IMPROVE_LOCK_STALE_MS = 4 * 60 * 60 * 1000;
|
|
12
13
|
export function improveLockPath(lockBaseDir) {
|
|
@@ -56,6 +57,12 @@ function tryAcquireImproveLockUnlocked(lockPath, staleAfterMs, skipIfLocked, onR
|
|
|
56
57
|
}
|
|
57
58
|
// Re-grabbed by another racer in the window — fall through and treat as held.
|
|
58
59
|
}
|
|
60
|
+
// A lock we cannot READ is a hard stop, never a reclaim candidate (#791): the
|
|
61
|
+
// holder may be alive and working, and we simply lack permission to see it.
|
|
62
|
+
// Stealing the lease here would let two improve runs mutate the same bundle.
|
|
63
|
+
if (probe.state === "inaccessible") {
|
|
64
|
+
throw new ConfigError(`improve lock exists but is not readable: ${describeInaccessiblePath(lockPath, probe.code)}.`, "DATA_DIR_UNREADABLE");
|
|
65
|
+
}
|
|
59
66
|
const rawContent = probe.state === "absent" ? undefined : probe.rawContent;
|
|
60
67
|
const lock = rawContent
|
|
61
68
|
? (() => {
|
|
@@ -8,6 +8,7 @@ import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontma
|
|
|
8
8
|
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
9
9
|
import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
|
|
10
10
|
import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
|
|
11
|
+
import { recordWrittenPath } from "../../../core/write-provenance.js";
|
|
11
12
|
import { isDerivedMemory, memoryIdentityRef, parseMemoryName, resolveParentRef } from "./derived-ref.js";
|
|
12
13
|
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
13
14
|
const records = collectDerivedMemories(stashDir, options.parentRef);
|
|
@@ -200,6 +201,8 @@ export function applyMemoryCleanup(stashDir, plan) {
|
|
|
200
201
|
continue; // no change
|
|
201
202
|
const newContent = assembleAsset(fm.data, resolvedBody);
|
|
202
203
|
fs.writeFileSync(candidate.filePath, newContent, "utf8");
|
|
204
|
+
// #652: relative-date resolution rewrites the asset in place.
|
|
205
|
+
recordWrittenPath(candidate.filePath);
|
|
203
206
|
relativeDatesResolved++;
|
|
204
207
|
}
|
|
205
208
|
catch (error) {
|
|
@@ -463,6 +466,10 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
|
463
466
|
const archivedPath = path.join(archiveDir, originalPath);
|
|
464
467
|
fs.mkdirSync(path.dirname(archivedPath), { recursive: true });
|
|
465
468
|
fs.renameSync(filePath, archivedPath);
|
|
469
|
+
// #652: an archive is a delete + a create. BOTH ends are journaled so the
|
|
470
|
+
// sync stages the removal of the original alongside the archived copy.
|
|
471
|
+
recordWrittenPath(filePath);
|
|
472
|
+
recordWrittenPath(archivedPath);
|
|
466
473
|
const archiveRef = path.relative(stashDir, archivedPath).replace(/\\/g, "/");
|
|
467
474
|
const auditPath = path.join(archiveDir, "cleanup.md");
|
|
468
475
|
const auditRef = path.relative(stashDir, auditPath).replace(/\\/g, "/");
|
|
@@ -480,6 +487,7 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
|
480
487
|
archivedPath: archiveRef,
|
|
481
488
|
}, "Archived derived memory for recoverable cleanup.\n");
|
|
482
489
|
fs.writeFileSync(auditPath, auditAsset, "utf8");
|
|
490
|
+
recordWrittenPath(auditPath);
|
|
483
491
|
return {
|
|
484
492
|
ref: candidate.ref,
|
|
485
493
|
parentRef: candidate.parentRef,
|
|
@@ -535,6 +543,7 @@ function appendBeliefStateTransitionLog(stashDir, transitions) {
|
|
|
535
543
|
}))
|
|
536
544
|
.join("\n");
|
|
537
545
|
fs.appendFileSync(logPath, `${lines}\n`, "utf8");
|
|
546
|
+
recordWrittenPath(logPath);
|
|
538
547
|
return logPath;
|
|
539
548
|
}
|
|
540
549
|
function priorBeliefStateForArchive(candidate) {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
import fs from "node:fs";
|
|
42
42
|
import path from "node:path";
|
|
43
43
|
import { resolveStashDir } from "../../core/common.js";
|
|
44
|
+
import { recordWrittenPath } from "../../core/write-provenance.js";
|
|
44
45
|
import { chatCompletion } from "../../llm/client.js";
|
|
45
46
|
/**
|
|
46
47
|
* Build a `RunContext` over the given carriers with the supplied memo (or `null`
|
|
@@ -73,6 +74,10 @@ function buildRunContext(carriers, memo) {
|
|
|
73
74
|
},
|
|
74
75
|
writeAsset(filePath, content) {
|
|
75
76
|
carriers.writeFile(filePath, content);
|
|
77
|
+
// #652: journal the write for the run's write-provenance set. Recorded
|
|
78
|
+
// here (not in the default `writeFile` carrier) so an injected IO seam is
|
|
79
|
+
// attributed identically to a real `fs` write.
|
|
80
|
+
recordWrittenPath(filePath);
|
|
76
81
|
memo?.set(memoKey(filePath), content);
|
|
77
82
|
},
|
|
78
83
|
noteAssetWrite(filePath) {
|
|
@@ -26,6 +26,7 @@ import path from "node:path";
|
|
|
26
26
|
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
27
27
|
import { assembleAsset } from "../../core/asset/asset-serialize.js";
|
|
28
28
|
import { conceptIdFromTypeName } from "../../core/asset/resolve-ref.js";
|
|
29
|
+
import { recordWrittenPath } from "../../core/write-provenance.js";
|
|
29
30
|
import { normalizeHarnessId } from "../../integrations/harnesses/index.js";
|
|
30
31
|
/**
|
|
31
32
|
* JSON Schema for the session-summary LLM call. Strict so providers that
|
|
@@ -240,6 +241,9 @@ export async function writeSessionAsset(data, stashDir, generate) {
|
|
|
240
241
|
const filePath = resolveSessionAssetPath(stashDir, harness, sessionId);
|
|
241
242
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
242
243
|
fs.writeFileSync(filePath, content, "utf8");
|
|
244
|
+
// #652: extract's session asset is written outside the proposal queue —
|
|
245
|
+
// journal it so the run's auto-sync stages it as one of its own writes.
|
|
246
|
+
recordWrittenPath(filePath);
|
|
243
247
|
return {
|
|
244
248
|
written: true,
|
|
245
249
|
filePath,
|
|
@@ -448,11 +448,17 @@ function parseInnerFrontmatterBlock(body) {
|
|
|
448
448
|
* File mutations triggered by the fixable base checks (`unquoted-colon`,
|
|
449
449
|
* `missing-updated`) are flushed to disk here when `ctx.fix` is set, and
|
|
450
450
|
* `ctx.raw` is updated in place so a caller can re-parse the post-fix content.
|
|
451
|
+
* A failed flush (read-only file, full disk) DOWNGRADES the optimistic
|
|
452
|
+
* `fixed: true` findings this call made to `fixed: "failed"` rather than
|
|
453
|
+
* throwing — an uncaught throw here used to kill the whole sweep and hide
|
|
454
|
+
* which earlier files had already been rewritten (issue #761).
|
|
451
455
|
*/
|
|
452
456
|
export function runBaseChecks(ctx) {
|
|
453
457
|
const issues = [];
|
|
454
458
|
let currentRaw = ctx.raw;
|
|
455
459
|
let modified = false;
|
|
460
|
+
/** Findings whose `fixed: true` is only true once the flush below succeeds. */
|
|
461
|
+
const pendingFixes = [];
|
|
456
462
|
// M8: Parse lint_skip from frontmatter for per-file rule suppression.
|
|
457
463
|
// Accept both an array (`lint_skip: [missing-ref, stale-path]`) and a
|
|
458
464
|
// single scalar (`lint_skip: missing-ref`). Non-string entries are coerced
|
|
@@ -469,12 +475,14 @@ export function runBaseChecks(ctx) {
|
|
|
469
475
|
if (ctx.fix) {
|
|
470
476
|
currentRaw = fixUnquotedColon(currentRaw);
|
|
471
477
|
modified = true;
|
|
472
|
-
|
|
478
|
+
const issue = {
|
|
473
479
|
file: ctx.relPath,
|
|
474
480
|
issue: "unquoted-colon",
|
|
475
481
|
detail: unquotedColonDetail,
|
|
476
482
|
fixed: true,
|
|
477
|
-
}
|
|
483
|
+
};
|
|
484
|
+
issues.push(issue);
|
|
485
|
+
pendingFixes.push(issue);
|
|
478
486
|
}
|
|
479
487
|
else {
|
|
480
488
|
issues.push({
|
|
@@ -498,12 +506,14 @@ export function runBaseChecks(ctx) {
|
|
|
498
506
|
}
|
|
499
507
|
currentRaw = fixMissingUpdated(currentRaw, mtime);
|
|
500
508
|
modified = true;
|
|
501
|
-
|
|
509
|
+
const issue = {
|
|
502
510
|
file: ctx.relPath,
|
|
503
511
|
issue: "missing-updated",
|
|
504
512
|
detail: `stamped updated: ${localDateStamp(mtime)}`,
|
|
505
513
|
fixed: true,
|
|
506
|
-
}
|
|
514
|
+
};
|
|
515
|
+
issues.push(issue);
|
|
516
|
+
pendingFixes.push(issue);
|
|
507
517
|
}
|
|
508
518
|
else {
|
|
509
519
|
issues.push({
|
|
@@ -515,9 +525,23 @@ export function runBaseChecks(ctx) {
|
|
|
515
525
|
}
|
|
516
526
|
}
|
|
517
527
|
if (modified) {
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
|
|
528
|
+
// Mirrors the two stub-delete call sites in `commands/lint/index.ts`
|
|
529
|
+
// (`appendMemoryStubIssue`/`appendWorkflowStubIssue`), which already report
|
|
530
|
+
// a failed mutation as `fixed: "failed"` instead of throwing. Without this
|
|
531
|
+
// the sweep aborted mid-run on the first unwritable file and the caller got
|
|
532
|
+
// an exception instead of a result naming the fixes that HAD landed.
|
|
533
|
+
try {
|
|
534
|
+
fs.writeFileSync(ctx.filePath, currentRaw, "utf8");
|
|
535
|
+
// Propagate the mutated raw back so subclasses can re-parse if needed
|
|
536
|
+
ctx.raw = currentRaw;
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
540
|
+
for (const issue of pendingFixes) {
|
|
541
|
+
issue.fixed = "failed";
|
|
542
|
+
issue.detail = `${issue.detail} — could not write fix: ${reason}`;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
521
545
|
}
|
|
522
546
|
// ── 3. stale-path ──────────────────────────────────────────────────────
|
|
523
547
|
// M3: checkStalePath returns all stale matches; push one issue per path.
|