@voybio/ace-swarm 2.4.3 → 2.4.6
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 +78 -0
- package/README.md +72 -532
- package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
- package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
- package/assets/agent-state/MODULES/registry.json +1 -4
- package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
- package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
- package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
- package/assets/agent-state/QUALITY_GATES.md +25 -9
- package/assets/agent-state/TEAL_CONFIG.md +2 -11
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +36 -9
- package/dist/astgrep-index.d.ts +7 -1
- package/dist/astgrep-index.js +66 -39
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +619 -10
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/gate-contract.d.ts +63 -0
- package/dist/gate-contract.js +178 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/runtime-executor.js +45 -2
- package/dist/runtime-tool-specs.d.ts +8 -1
- package/dist/runtime-tool-specs.js +19 -1
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/schemas.js +16 -15
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +87 -9
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/gate-contract-migration.d.ts +10 -0
- package/dist/store/gate-contract-migration.js +413 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +107 -23
- package/dist/tools-framework.js +146 -40
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +125 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +5 -1
- package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
- package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
- package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
- package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoalRepository — durable storage for the ACE Goal Engine (WS-F).
|
|
3
|
+
*
|
|
4
|
+
* Persists one {@link GoalRecord} plus its active {@link TaskPlan} per goal, and
|
|
5
|
+
* a lightweight index for listing. Storage is plain JSON under
|
|
6
|
+
* `<workspaceRoot>/agent-state/goals/`, written atomically (temp + rename) so a
|
|
7
|
+
* crash mid-write never leaves a torn record — the property WS-F's resume path
|
|
8
|
+
* depends on. The repository is intentionally root-parameterized (rather than
|
|
9
|
+
* bound to the packed store) so the Goal Engine and its tests can drive it
|
|
10
|
+
* hermetically against a temp workspace.
|
|
11
|
+
*
|
|
12
|
+
* Layout:
|
|
13
|
+
* agent-state/goals/index.json { version, updated_at, goals: [...] }
|
|
14
|
+
* agent-state/goals/<goal_id>.json GoalRecord
|
|
15
|
+
* agent-state/goals/<goal_id>.plan.json TaskPlan (rewritten every transition)
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
export function newGoalId() {
|
|
21
|
+
return `goal-${randomUUID()}`;
|
|
22
|
+
}
|
|
23
|
+
function nowIso() {
|
|
24
|
+
return new Date().toISOString();
|
|
25
|
+
}
|
|
26
|
+
export class GoalRepository {
|
|
27
|
+
workspaceRoot;
|
|
28
|
+
dir;
|
|
29
|
+
constructor(workspaceRoot) {
|
|
30
|
+
this.workspaceRoot = workspaceRoot;
|
|
31
|
+
this.dir = join(workspaceRoot, "agent-state", "goals");
|
|
32
|
+
}
|
|
33
|
+
ensureDir() {
|
|
34
|
+
if (!existsSync(this.dir)) {
|
|
35
|
+
mkdirSync(this.dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
goalPath(goalId) {
|
|
39
|
+
return join(this.dir, `${goalId}.json`);
|
|
40
|
+
}
|
|
41
|
+
planPath(goalId) {
|
|
42
|
+
return join(this.dir, `${goalId}.plan.json`);
|
|
43
|
+
}
|
|
44
|
+
indexPath() {
|
|
45
|
+
return join(this.dir, "index.json");
|
|
46
|
+
}
|
|
47
|
+
writeAtomic(path, value) {
|
|
48
|
+
this.ensureDir();
|
|
49
|
+
const tmp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
50
|
+
writeFileSync(tmp, JSON.stringify(value, null, 2));
|
|
51
|
+
renameSync(tmp, path);
|
|
52
|
+
}
|
|
53
|
+
readJson(path) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
readIndex() {
|
|
64
|
+
const stored = this.readJson(this.indexPath());
|
|
65
|
+
if (!stored || stored.version !== 1 || !Array.isArray(stored.goals)) {
|
|
66
|
+
return { version: 1, updated_at: nowIso(), goals: [] };
|
|
67
|
+
}
|
|
68
|
+
return stored;
|
|
69
|
+
}
|
|
70
|
+
upsertIndex(goal) {
|
|
71
|
+
const index = this.readIndex();
|
|
72
|
+
const entry = {
|
|
73
|
+
goal_id: goal.goal_id,
|
|
74
|
+
status: goal.status,
|
|
75
|
+
objective: goal.objective,
|
|
76
|
+
updated_at: goal.updated_at,
|
|
77
|
+
};
|
|
78
|
+
const next = index.goals.filter((row) => row.goal_id !== goal.goal_id);
|
|
79
|
+
next.push(entry);
|
|
80
|
+
next.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
|
|
81
|
+
this.writeAtomic(this.indexPath(), {
|
|
82
|
+
version: 1,
|
|
83
|
+
updated_at: nowIso(),
|
|
84
|
+
goals: next,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Persist a goal record and refresh the index in one call. */
|
|
88
|
+
saveGoal(goal) {
|
|
89
|
+
const record = { ...goal, updated_at: nowIso() };
|
|
90
|
+
this.writeAtomic(this.goalPath(record.goal_id), record);
|
|
91
|
+
this.upsertIndex(record);
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
getGoal(goalId) {
|
|
95
|
+
return this.readJson(this.goalPath(goalId));
|
|
96
|
+
}
|
|
97
|
+
savePlan(goalId, plan) {
|
|
98
|
+
this.writeAtomic(this.planPath(goalId), plan);
|
|
99
|
+
}
|
|
100
|
+
getPlan(goalId) {
|
|
101
|
+
return this.readJson(this.planPath(goalId));
|
|
102
|
+
}
|
|
103
|
+
listGoals(filter) {
|
|
104
|
+
return this.readIndex().goals.filter((row) => filter?.status ? row.status === filter.status : true);
|
|
105
|
+
}
|
|
106
|
+
/** Delete a goal, its plan, and its index entry. Returns true if present. */
|
|
107
|
+
deleteGoal(goalId) {
|
|
108
|
+
const goalPath = this.goalPath(goalId);
|
|
109
|
+
const existed = existsSync(goalPath);
|
|
110
|
+
for (const path of [goalPath, this.planPath(goalId)]) {
|
|
111
|
+
if (existsSync(path))
|
|
112
|
+
rmSync(path);
|
|
113
|
+
}
|
|
114
|
+
const index = this.readIndex();
|
|
115
|
+
this.writeAtomic(this.indexPath(), {
|
|
116
|
+
version: 1,
|
|
117
|
+
updated_at: nowIso(),
|
|
118
|
+
goals: index.goals.filter((row) => row.goal_id !== goalId),
|
|
119
|
+
});
|
|
120
|
+
return existed;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Rebuild the index from the on-disk goal files. Cheap self-heal used at
|
|
124
|
+
* startup so a hand-deleted or partially-written index never hides live goals.
|
|
125
|
+
*/
|
|
126
|
+
rebuildIndex() {
|
|
127
|
+
this.ensureDir();
|
|
128
|
+
const goals = [];
|
|
129
|
+
for (const name of readdirSync(this.dir)) {
|
|
130
|
+
if (!name.endsWith(".json") || name.endsWith(".plan.json") || name === "index.json") {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const record = this.readJson(join(this.dir, name));
|
|
134
|
+
if (record?.goal_id) {
|
|
135
|
+
goals.push({
|
|
136
|
+
goal_id: record.goal_id,
|
|
137
|
+
status: record.status,
|
|
138
|
+
objective: record.objective,
|
|
139
|
+
updated_at: record.updated_at,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
goals.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
|
|
144
|
+
const file = { version: 1, updated_at: nowIso(), goals };
|
|
145
|
+
this.writeAtomic(this.indexPath(), file);
|
|
146
|
+
return file;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function defaultGoalBudgets() {
|
|
150
|
+
return {
|
|
151
|
+
max_wall_clock_ms: 4 * 60 * 60 * 1000, // 4h
|
|
152
|
+
max_ticks: 200,
|
|
153
|
+
max_replans: 3,
|
|
154
|
+
max_step_retries: 1,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=goal-repository.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export type PlaybookSection = "strategies" | "tool_usage" | "failure_modes" | "verification" | "env_facts";
|
|
2
|
+
export type PlaybookBulletStatus = "active" | "quarantined" | "retired";
|
|
3
|
+
export interface PlaybookScope {
|
|
4
|
+
role?: string;
|
|
5
|
+
model_class?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
tool?: string;
|
|
8
|
+
reason_code?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PlaybookBullet {
|
|
11
|
+
bullet_id: string;
|
|
12
|
+
section: PlaybookSection;
|
|
13
|
+
scope: PlaybookScope;
|
|
14
|
+
content: string;
|
|
15
|
+
helpful_count: number;
|
|
16
|
+
harmful_count: number;
|
|
17
|
+
observed_count: number;
|
|
18
|
+
status: PlaybookBulletStatus;
|
|
19
|
+
evidence_refs: string[];
|
|
20
|
+
provenance: {
|
|
21
|
+
source: "reflector" | "llm_reflector" | "operator" | "migration";
|
|
22
|
+
run_id?: string;
|
|
23
|
+
};
|
|
24
|
+
created_at: string;
|
|
25
|
+
updated_at: string;
|
|
26
|
+
}
|
|
27
|
+
export type PlaybookDeltaOp = {
|
|
28
|
+
type: "ADD";
|
|
29
|
+
section: PlaybookSection;
|
|
30
|
+
scope: PlaybookScope;
|
|
31
|
+
content: string;
|
|
32
|
+
evidence_refs: string[];
|
|
33
|
+
provenance: PlaybookBullet["provenance"];
|
|
34
|
+
} | {
|
|
35
|
+
type: "TAG";
|
|
36
|
+
bullet_id: string;
|
|
37
|
+
tag: "helpful" | "harmful";
|
|
38
|
+
};
|
|
39
|
+
export interface PlaybookConfig {
|
|
40
|
+
max_active_total?: number;
|
|
41
|
+
max_active_per_section?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface PlaybookIndexEntry {
|
|
44
|
+
bullet_id: string;
|
|
45
|
+
section: PlaybookSection;
|
|
46
|
+
status: PlaybookBulletStatus;
|
|
47
|
+
updated_at: string;
|
|
48
|
+
}
|
|
49
|
+
export interface PlaybookIndexFile {
|
|
50
|
+
version: 1;
|
|
51
|
+
updated_at: string;
|
|
52
|
+
bullets: PlaybookIndexEntry[];
|
|
53
|
+
}
|
|
54
|
+
export interface PlaybookApplyResult {
|
|
55
|
+
added: number;
|
|
56
|
+
tagged: number;
|
|
57
|
+
refused: Array<{
|
|
58
|
+
reason_code: string;
|
|
59
|
+
op: PlaybookDeltaOp;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
export declare const PLAYBOOK_SECTIONS: readonly PlaybookSection[];
|
|
63
|
+
export declare const PLAYBOOK_STATUSES: readonly PlaybookBulletStatus[];
|
|
64
|
+
export declare function isPlaybookSection(value: string): value is PlaybookSection;
|
|
65
|
+
export declare function isPlaybookStatus(value: string): value is PlaybookBulletStatus;
|
|
66
|
+
export declare class PlaybookRepository {
|
|
67
|
+
private readonly workspaceRoot;
|
|
68
|
+
private readonly dir;
|
|
69
|
+
private migrated;
|
|
70
|
+
constructor(workspaceRoot: string);
|
|
71
|
+
get root(): string;
|
|
72
|
+
private useStore;
|
|
73
|
+
private ensureDir;
|
|
74
|
+
private bulletRelPath;
|
|
75
|
+
private bulletPath;
|
|
76
|
+
private indexRelPath;
|
|
77
|
+
private indexPath;
|
|
78
|
+
private markerRelPath;
|
|
79
|
+
private markerPath;
|
|
80
|
+
private artifactKey;
|
|
81
|
+
private writeText;
|
|
82
|
+
private writeJson;
|
|
83
|
+
private readText;
|
|
84
|
+
private existsRel;
|
|
85
|
+
private readJson;
|
|
86
|
+
private listBulletRelPaths;
|
|
87
|
+
private migrateOnce;
|
|
88
|
+
readIndex(): PlaybookIndexFile;
|
|
89
|
+
private upsertIndex;
|
|
90
|
+
saveBullet(bullet: PlaybookBullet): PlaybookBullet;
|
|
91
|
+
createBullet(input: Omit<PlaybookBullet, "bullet_id" | "helpful_count" | "harmful_count" | "observed_count" | "status" | "created_at" | "updated_at">): PlaybookBullet;
|
|
92
|
+
getBullet(bulletId: string): PlaybookBullet | undefined;
|
|
93
|
+
listBullets(filter?: {
|
|
94
|
+
section?: PlaybookSection;
|
|
95
|
+
status?: PlaybookBulletStatus;
|
|
96
|
+
scope?: PlaybookScope;
|
|
97
|
+
}): PlaybookBullet[];
|
|
98
|
+
applyDeltaOps(ops: PlaybookDeltaOp[], config?: PlaybookConfig): PlaybookApplyResult;
|
|
99
|
+
rebuildIndex(): PlaybookIndexFile;
|
|
100
|
+
}
|
|
101
|
+
export declare function parseLessonsMarkdown(markdown: string): Array<Omit<PlaybookBullet, "bullet_id" | "helpful_count" | "harmful_count" | "observed_count" | "status" | "created_at" | "updated_at">>;
|
|
102
|
+
export declare function renderPlaybookMarkdown(repo: PlaybookRepository, filter?: {
|
|
103
|
+
section?: PlaybookSection;
|
|
104
|
+
status?: PlaybookBulletStatus;
|
|
105
|
+
}): string;
|
|
106
|
+
export declare function materializePlaybookMarkdown(workspaceRoot: string): string;
|
|
107
|
+
//# sourceMappingURL=playbook-repository.d.ts.map
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { operationalArtifactKey, writeOperationalArtifactSync, } from "../store-artifacts.js";
|
|
5
|
+
import { listStoreKeysSync, readStoreBlobSync, storeExistsSync, } from "../store-snapshot.js";
|
|
6
|
+
export const PLAYBOOK_SECTIONS = ["strategies", "tool_usage", "failure_modes", "verification", "env_facts"];
|
|
7
|
+
export const PLAYBOOK_STATUSES = ["active", "quarantined", "retired"];
|
|
8
|
+
const SECTIONS = PLAYBOOK_SECTIONS;
|
|
9
|
+
export function isPlaybookSection(value) {
|
|
10
|
+
return PLAYBOOK_SECTIONS.some((section) => section === value);
|
|
11
|
+
}
|
|
12
|
+
export function isPlaybookStatus(value) {
|
|
13
|
+
return PLAYBOOK_STATUSES.some((status) => status === value);
|
|
14
|
+
}
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function newBulletId() {
|
|
19
|
+
return `pb-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
20
|
+
}
|
|
21
|
+
function truncateContent(content) {
|
|
22
|
+
return content.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
23
|
+
}
|
|
24
|
+
function scopeMatches(bulletScope, filter) {
|
|
25
|
+
if (!filter)
|
|
26
|
+
return true;
|
|
27
|
+
return Object.entries(filter).every(([key, value]) => value === undefined || bulletScope[key] === undefined || bulletScope[key] === value);
|
|
28
|
+
}
|
|
29
|
+
export class PlaybookRepository {
|
|
30
|
+
workspaceRoot;
|
|
31
|
+
dir;
|
|
32
|
+
migrated = false;
|
|
33
|
+
constructor(workspaceRoot) {
|
|
34
|
+
this.workspaceRoot = workspaceRoot;
|
|
35
|
+
this.dir = join(workspaceRoot, "agent-state", "playbook");
|
|
36
|
+
}
|
|
37
|
+
get root() {
|
|
38
|
+
return this.workspaceRoot;
|
|
39
|
+
}
|
|
40
|
+
useStore() {
|
|
41
|
+
return storeExistsSync(this.workspaceRoot);
|
|
42
|
+
}
|
|
43
|
+
ensureDir() {
|
|
44
|
+
if (!this.useStore() && !existsSync(this.dir))
|
|
45
|
+
mkdirSync(this.dir, { recursive: true });
|
|
46
|
+
this.migrateOnce();
|
|
47
|
+
}
|
|
48
|
+
bulletRelPath(bulletId) {
|
|
49
|
+
return `agent-state/playbook/${bulletId}.json`;
|
|
50
|
+
}
|
|
51
|
+
bulletPath(bulletId) {
|
|
52
|
+
return join(this.workspaceRoot, this.bulletRelPath(bulletId));
|
|
53
|
+
}
|
|
54
|
+
indexRelPath() {
|
|
55
|
+
return "agent-state/playbook/__index.json";
|
|
56
|
+
}
|
|
57
|
+
indexPath() {
|
|
58
|
+
return join(this.workspaceRoot, this.indexRelPath());
|
|
59
|
+
}
|
|
60
|
+
markerRelPath() {
|
|
61
|
+
return "agent-state/playbook/__migrated";
|
|
62
|
+
}
|
|
63
|
+
markerPath() {
|
|
64
|
+
return join(this.workspaceRoot, this.markerRelPath());
|
|
65
|
+
}
|
|
66
|
+
artifactKey(relPath) {
|
|
67
|
+
return operationalArtifactKey(relPath);
|
|
68
|
+
}
|
|
69
|
+
writeText(relPath, content) {
|
|
70
|
+
if (this.useStore()) {
|
|
71
|
+
return writeOperationalArtifactSync(this.workspaceRoot, relPath, content);
|
|
72
|
+
}
|
|
73
|
+
const path = join(this.workspaceRoot, relPath);
|
|
74
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
75
|
+
const tmp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
76
|
+
writeFileSync(tmp, content, "utf8");
|
|
77
|
+
renameSync(tmp, path);
|
|
78
|
+
return path;
|
|
79
|
+
}
|
|
80
|
+
writeJson(relPath, value) {
|
|
81
|
+
return this.writeText(relPath, JSON.stringify(value, null, 2));
|
|
82
|
+
}
|
|
83
|
+
readText(relPath) {
|
|
84
|
+
if (this.useStore()) {
|
|
85
|
+
const storeText = readStoreBlobSync(this.workspaceRoot, this.artifactKey(relPath));
|
|
86
|
+
if (typeof storeText === "string")
|
|
87
|
+
return storeText;
|
|
88
|
+
}
|
|
89
|
+
const path = join(this.workspaceRoot, relPath);
|
|
90
|
+
if (!existsSync(path))
|
|
91
|
+
return undefined;
|
|
92
|
+
return readFileSync(path, "utf8");
|
|
93
|
+
}
|
|
94
|
+
existsRel(relPath) {
|
|
95
|
+
if (this.useStore() && typeof readStoreBlobSync(this.workspaceRoot, this.artifactKey(relPath)) === "string") {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return existsSync(join(this.workspaceRoot, relPath));
|
|
99
|
+
}
|
|
100
|
+
readJson(relPath) {
|
|
101
|
+
const raw = this.readText(relPath);
|
|
102
|
+
if (typeof raw !== "string")
|
|
103
|
+
return undefined;
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
listBulletRelPaths() {
|
|
112
|
+
const rels = new Set();
|
|
113
|
+
if (this.useStore()) {
|
|
114
|
+
const prefix = this.artifactKey("agent-state/playbook/");
|
|
115
|
+
for (const key of listStoreKeysSync(this.workspaceRoot, prefix)) {
|
|
116
|
+
const relPath = key.slice("state/artifacts/".length);
|
|
117
|
+
if (relPath.endsWith(".json") && !relPath.endsWith("/__index.json")) {
|
|
118
|
+
rels.add(relPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (existsSync(this.dir)) {
|
|
123
|
+
for (const name of readdirSync(this.dir)) {
|
|
124
|
+
if (name.endsWith(".json") && name !== "__index.json") {
|
|
125
|
+
rels.add(`agent-state/playbook/${name}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...rels].sort((a, b) => a.localeCompare(b));
|
|
130
|
+
}
|
|
131
|
+
migrateOnce() {
|
|
132
|
+
if (this.migrated || this.existsRel(this.markerRelPath())) {
|
|
133
|
+
this.migrated = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.migrated = true;
|
|
137
|
+
const lessons = this.readText("agent-state/lessons.md");
|
|
138
|
+
if (typeof lessons === "string") {
|
|
139
|
+
for (const bullet of parseLessonsMarkdown(lessons)) {
|
|
140
|
+
this.createBullet(bullet);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this.writeText(this.markerRelPath(), `${nowIso()}\n`);
|
|
144
|
+
}
|
|
145
|
+
readIndex() {
|
|
146
|
+
this.ensureDir();
|
|
147
|
+
const stored = this.readJson(this.indexRelPath());
|
|
148
|
+
if (!stored || stored.version !== 1 || !Array.isArray(stored.bullets)) {
|
|
149
|
+
return this.rebuildIndex();
|
|
150
|
+
}
|
|
151
|
+
return stored;
|
|
152
|
+
}
|
|
153
|
+
upsertIndex(bullet) {
|
|
154
|
+
const index = this.readIndex();
|
|
155
|
+
const next = index.bullets.filter((row) => row.bullet_id !== bullet.bullet_id);
|
|
156
|
+
next.push({
|
|
157
|
+
bullet_id: bullet.bullet_id,
|
|
158
|
+
section: bullet.section,
|
|
159
|
+
status: bullet.status,
|
|
160
|
+
updated_at: bullet.updated_at,
|
|
161
|
+
});
|
|
162
|
+
next.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
|
|
163
|
+
this.writeJson(this.indexRelPath(), { version: 1, updated_at: nowIso(), bullets: next });
|
|
164
|
+
}
|
|
165
|
+
saveBullet(bullet) {
|
|
166
|
+
this.ensureDir();
|
|
167
|
+
const record = { ...bullet, content: truncateContent(bullet.content), updated_at: nowIso() };
|
|
168
|
+
this.writeJson(this.bulletRelPath(record.bullet_id), record);
|
|
169
|
+
this.upsertIndex(record);
|
|
170
|
+
return record;
|
|
171
|
+
}
|
|
172
|
+
createBullet(input) {
|
|
173
|
+
const at = nowIso();
|
|
174
|
+
return this.saveBullet({
|
|
175
|
+
bullet_id: newBulletId(),
|
|
176
|
+
section: input.section,
|
|
177
|
+
scope: input.scope,
|
|
178
|
+
content: input.content,
|
|
179
|
+
helpful_count: 0,
|
|
180
|
+
harmful_count: 0,
|
|
181
|
+
observed_count: input.provenance.source === "llm_reflector" ? 0 : 1,
|
|
182
|
+
// Evidence-free notes from every producer are retained but cannot be injected
|
|
183
|
+
// until evidence is attached through a curated path.
|
|
184
|
+
status: input.evidence_refs.length === 0 ? "quarantined" : "active",
|
|
185
|
+
evidence_refs: input.evidence_refs.slice(0, 5),
|
|
186
|
+
provenance: input.provenance,
|
|
187
|
+
created_at: at,
|
|
188
|
+
updated_at: at,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
getBullet(bulletId) {
|
|
192
|
+
this.ensureDir();
|
|
193
|
+
return this.readJson(this.bulletRelPath(bulletId));
|
|
194
|
+
}
|
|
195
|
+
listBullets(filter) {
|
|
196
|
+
return this.readIndex().bullets
|
|
197
|
+
.filter((row) => (!filter?.section || row.section === filter.section) && (!filter?.status || row.status === filter.status))
|
|
198
|
+
.flatMap((row) => {
|
|
199
|
+
const bullet = this.getBullet(row.bullet_id);
|
|
200
|
+
return bullet && scopeMatches(bullet.scope, filter?.scope) ? [bullet] : [];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
applyDeltaOps(ops, config = {}) {
|
|
204
|
+
const result = { added: 0, tagged: 0, refused: [] };
|
|
205
|
+
for (const op of ops) {
|
|
206
|
+
if (op.type === "TAG") {
|
|
207
|
+
const bullet = this.getBullet(op.bullet_id);
|
|
208
|
+
if (!bullet)
|
|
209
|
+
continue;
|
|
210
|
+
this.saveBullet({
|
|
211
|
+
...bullet,
|
|
212
|
+
helpful_count: bullet.helpful_count + (op.tag === "helpful" ? 1 : 0),
|
|
213
|
+
harmful_count: bullet.harmful_count + (op.tag === "harmful" ? 1 : 0),
|
|
214
|
+
});
|
|
215
|
+
result.tagged += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const active = this.listBullets({ status: "active", section: op.section });
|
|
219
|
+
const perSectionCap = config.max_active_per_section ?? 64;
|
|
220
|
+
const totalCap = config.max_active_total ?? 200;
|
|
221
|
+
if (active.length >= perSectionCap || this.listBullets({ status: "active" }).length >= totalCap) {
|
|
222
|
+
result.refused.push({ reason_code: "playbook_section_full", op });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
this.createBullet(op);
|
|
226
|
+
result.added += 1;
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
rebuildIndex() {
|
|
231
|
+
if (!this.useStore() && !existsSync(this.dir))
|
|
232
|
+
mkdirSync(this.dir, { recursive: true });
|
|
233
|
+
const bullets = [];
|
|
234
|
+
for (const relPath of this.listBulletRelPaths()) {
|
|
235
|
+
const bullet = this.readJson(relPath);
|
|
236
|
+
if (bullet?.bullet_id && SECTIONS.includes(bullet.section)) {
|
|
237
|
+
bullets.push({
|
|
238
|
+
bullet_id: bullet.bullet_id,
|
|
239
|
+
section: bullet.section,
|
|
240
|
+
status: bullet.status,
|
|
241
|
+
updated_at: bullet.updated_at,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
bullets.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
|
|
246
|
+
const file = { version: 1, updated_at: nowIso(), bullets };
|
|
247
|
+
this.writeJson(this.indexRelPath(), file);
|
|
248
|
+
return file;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function parseLessonsMarkdown(markdown) {
|
|
252
|
+
return markdown
|
|
253
|
+
.split(/^## Lesson\s+.*$/m)
|
|
254
|
+
.map((block) => block.trim())
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map((block) => {
|
|
257
|
+
const field = (name) => {
|
|
258
|
+
const match = block.match(new RegExp(`(?:^|\\n)\\*\\*${name}:\\*\\*\\s*([\\s\\S]*?)(?=\\n\\*\\*|$)`, "i"));
|
|
259
|
+
return match?.[1]?.trim() ?? "";
|
|
260
|
+
};
|
|
261
|
+
const guardrail = field("Guardrail");
|
|
262
|
+
const verification = field("Verification Check");
|
|
263
|
+
return {
|
|
264
|
+
section: "failure_modes",
|
|
265
|
+
scope: {},
|
|
266
|
+
content: truncateContent(`${guardrail}${verification ? ` Verify: ${verification}` : ""}`),
|
|
267
|
+
evidence_refs: [],
|
|
268
|
+
provenance: { source: "migration" },
|
|
269
|
+
};
|
|
270
|
+
})
|
|
271
|
+
.filter((bullet) => bullet.content.length > 0);
|
|
272
|
+
}
|
|
273
|
+
export function renderPlaybookMarkdown(repo, filter = {}) {
|
|
274
|
+
const bullets = repo
|
|
275
|
+
.listBullets(filter)
|
|
276
|
+
.sort((a, b) => a.section.localeCompare(b.section) ||
|
|
277
|
+
a.status.localeCompare(b.status) ||
|
|
278
|
+
a.bullet_id.localeCompare(b.bullet_id));
|
|
279
|
+
const lines = ["# PLAYBOOK", "", "_Derived from ACEPACK-backed `agent-state/playbook/*.json`; regenerate rather than hand-edit._", ""];
|
|
280
|
+
for (const status of ["active", "quarantined", "retired"]) {
|
|
281
|
+
const statusBullets = bullets.filter((bullet) => bullet.status === status);
|
|
282
|
+
if (statusBullets.length === 0)
|
|
283
|
+
continue;
|
|
284
|
+
lines.push(`## ${status}`, "");
|
|
285
|
+
for (const section of SECTIONS) {
|
|
286
|
+
const sectionBullets = statusBullets.filter((bullet) => bullet.section === section);
|
|
287
|
+
if (sectionBullets.length === 0)
|
|
288
|
+
continue;
|
|
289
|
+
lines.push(`### ${section}`, "");
|
|
290
|
+
for (const bullet of sectionBullets) {
|
|
291
|
+
const scope = Object.keys(bullet.scope).length > 0 ? ` scope=${JSON.stringify(bullet.scope)}` : "";
|
|
292
|
+
lines.push(`- [${bullet.bullet_id}] ${bullet.content} ` +
|
|
293
|
+
`(helpful=${bullet.helpful_count}, harmful=${bullet.harmful_count}, observed=${bullet.observed_count}${scope})`);
|
|
294
|
+
}
|
|
295
|
+
lines.push("");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (bullets.length === 0) {
|
|
299
|
+
lines.push("_No playbook bullets recorded._", "");
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
export function materializePlaybookMarkdown(workspaceRoot) {
|
|
304
|
+
const repo = new PlaybookRepository(workspaceRoot);
|
|
305
|
+
const relPath = "agent-state/PLAYBOOK.md";
|
|
306
|
+
if (storeExistsSync(workspaceRoot)) {
|
|
307
|
+
return writeOperationalArtifactSync(workspaceRoot, relPath, renderPlaybookMarkdown(repo));
|
|
308
|
+
}
|
|
309
|
+
const path = join(workspaceRoot, relPath);
|
|
310
|
+
mkdirSync(join(workspaceRoot, "agent-state"), { recursive: true });
|
|
311
|
+
writeFileSync(path, renderPlaybookMarkdown(repo), "utf8");
|
|
312
|
+
return path;
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=playbook-repository.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { openStore } from "./ace-packed-store.js";
|
|
4
|
-
import { getWorkspaceStorePath, invalidateStoreSnapshotCache, readStoreBlobSync, toVirtualStorePath, } from "./store-snapshot.js";
|
|
4
|
+
import { getWorkspaceStorePath, invalidateStoreSnapshotCache, readStoreBlobSync, toVirtualStorePath, validateChunkBounds, } from "./store-snapshot.js";
|
|
5
5
|
import { withStoreWriteCoordinator } from "./write-coordinator.js";
|
|
6
6
|
import { HEADER_SIZE, HEADER_SIZE_V1, STORE_VERSION, MAGIC } from "./types.js";
|
|
7
|
+
import { crc32 } from "./crc32.js";
|
|
7
8
|
// ── v2 header constants (mirrors ace-packed-store.ts) ─────────────────────────
|
|
8
9
|
const H_KV_IDX_OFF = 16;
|
|
9
10
|
const H_KV_IDX_LEN = 24;
|
|
@@ -125,6 +126,29 @@ function defaultStoreBuffer() {
|
|
|
125
126
|
buf.set(indexBytes, kvIdxOff);
|
|
126
127
|
return buf;
|
|
127
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Validates the structural invariants of a parsed v2 header before any code
|
|
131
|
+
* splices bytes out of the file using it. A corrupt header (e.g. kvChunkEnd
|
|
132
|
+
* pointing past EOF) must never be trusted to build a new file — that would
|
|
133
|
+
* bake a zero-filled/garbage region into the *new* store and spread
|
|
134
|
+
* corruption to every key, not just the bad one.
|
|
135
|
+
*/
|
|
136
|
+
function validateStoreHeaderInvariants(hdr, index, fileLength) {
|
|
137
|
+
if (!(HEADER_SIZE <= hdr.kvChunkEnd && hdr.kvChunkEnd <= hdr.evtOffset && hdr.evtOffset <= hdr.kvIndexOffset && hdr.kvIndexOffset <= fileLength)) {
|
|
138
|
+
return `header region ordering invalid (kvChunkEnd=${hdr.kvChunkEnd}, evtOffset=${hdr.evtOffset}, kvIndexOffset=${hdr.kvIndexOffset}, fileLength=${fileLength})`;
|
|
139
|
+
}
|
|
140
|
+
for (const [key, entry] of Object.entries(index)) {
|
|
141
|
+
if (!validateChunkBounds(entry, hdr.kvChunkEnd, HEADER_SIZE)) {
|
|
142
|
+
return `index entry '${key}' out of KV region bounds (offset=${entry.offset}, length=${entry.length}, kvChunkEnd=${hdr.kvChunkEnd})`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
function quarantineCorruptStore(storePath, rawFile) {
|
|
148
|
+
const quarantinePath = `${storePath}.corrupt-${Date.now()}`;
|
|
149
|
+
writeFileSync(quarantinePath, rawFile);
|
|
150
|
+
return quarantinePath;
|
|
151
|
+
}
|
|
128
152
|
function readStoreBuffer(storePath) {
|
|
129
153
|
const rawFile = existsSync(storePath) ? readFileSync(storePath) : defaultStoreBuffer();
|
|
130
154
|
// Buffer (from readFileSync) is a Uint8Array subclass — wrap to get a plain Uint8Array view
|
|
@@ -135,11 +159,25 @@ function readStoreBuffer(storePath) {
|
|
|
135
159
|
return { file: def, kvChunkEnd: HEADER_SIZE, evtBytes: new Uint8Array(4), evtCount: 0, evtBaseId: 0, index: {} };
|
|
136
160
|
}
|
|
137
161
|
const hdr = readStoreHeader(file.subarray(0, Math.max(HEADER_SIZE, hdrSize)));
|
|
162
|
+
if (hdr.kvIndexOffset < 0 ||
|
|
163
|
+
hdr.kvIndexLength < 0 ||
|
|
164
|
+
hdr.kvIndexOffset + hdr.kvIndexLength > file.length ||
|
|
165
|
+
hdr.evtOffset < 0 ||
|
|
166
|
+
hdr.evtLength < 0 ||
|
|
167
|
+
hdr.evtOffset + hdr.evtLength > file.length) {
|
|
168
|
+
const quarantinePath = quarantineCorruptStore(storePath, rawFile);
|
|
169
|
+
throw new Error(`AcePackedStore artifact write refused: header region exceeds file bounds — quarantined corrupt store at ${quarantinePath}, original left in place. Run 'ace repair' or 'ace fsck --repair'.`);
|
|
170
|
+
}
|
|
138
171
|
const evtBytes = hdr.evtLength > 0
|
|
139
172
|
? file.slice(hdr.evtOffset, hdr.evtOffset + hdr.evtLength)
|
|
140
173
|
: new Uint8Array(4); // empty event section
|
|
141
174
|
const indexRaw = file.subarray(hdr.kvIndexOffset, hdr.kvIndexOffset + hdr.kvIndexLength);
|
|
142
175
|
const index = JSON.parse(DECODE.decode(indexRaw));
|
|
176
|
+
const invariantError = validateStoreHeaderInvariants(hdr, index, file.length);
|
|
177
|
+
if (invariantError) {
|
|
178
|
+
const quarantinePath = quarantineCorruptStore(storePath, rawFile);
|
|
179
|
+
throw new Error(`AcePackedStore artifact write refused: ${invariantError} — quarantined corrupt store at ${quarantinePath}, original left in place. Run 'ace repair' or 'ace fsck --repair'.`);
|
|
180
|
+
}
|
|
143
181
|
return { file, kvChunkEnd: hdr.kvChunkEnd, evtBytes, evtCount: hdr.evtCount, evtBaseId: hdr.evtBaseId, index };
|
|
144
182
|
}
|
|
145
183
|
export function operationalArtifactKey(relPath) {
|
|
@@ -169,7 +207,11 @@ export function writeStoreBlobsSync(workspaceRoot, blobs) {
|
|
|
169
207
|
const nextIndex = { ...index };
|
|
170
208
|
let cursor = kvChunkEnd;
|
|
171
209
|
for (const entry of encoded) {
|
|
172
|
-
nextIndex[entry.key] = {
|
|
210
|
+
nextIndex[entry.key] = {
|
|
211
|
+
offset: cursor,
|
|
212
|
+
length: entry.contentBytes.length,
|
|
213
|
+
crc: crc32(entry.contentBytes),
|
|
214
|
+
};
|
|
173
215
|
cursor += 4 + entry.contentBytes.length;
|
|
174
216
|
}
|
|
175
217
|
const indexBytes = TEXT.encode(JSON.stringify(nextIndex));
|
|
@@ -31,6 +31,7 @@ const OFFLINE_SYNC_HELPER_SOURCES = new Set([
|
|
|
31
31
|
"src/astgrep-index.ts",
|
|
32
32
|
"src/index-store.ts",
|
|
33
33
|
"src/tools-files.ts",
|
|
34
|
+
"src/source-mutation-coordinator.ts",
|
|
34
35
|
]);
|
|
35
36
|
const TOOL_RUNTIME_SOURCE_HINT = /(?:^|\/)(?:tools-[^/]+|tools-framework|runtime-[^/]+|tracker-[^/]+|workspace-manager|status-events|job-scheduler|run-ledger|runtime-profile|handoff-registry|astgrep-index)\.ts$/;
|
|
36
37
|
const TASK_KEY_TO_FILE = {
|