@abelxiaoxing/cadence 1.2.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/package.json +5 -3
- package/prompts/abel-design.md +71 -17
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-patch.ts +2 -1
- package/src/canonical.ts +16 -0
- package/src/change-contract.ts +268 -0
- package/src/child-session.ts +4 -3
- package/src/contracts.ts +141 -33
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +148 -31
- package/src/design-control.ts +83 -15
- package/src/design-journal.ts +216 -72
- package/src/durable-workflow.ts +4385 -0
- package/src/implement-graph.ts +11 -16
- package/src/index.ts +399 -1079
- package/src/isolation-backend.ts +84 -23
- package/src/openspec-cli.ts +494 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +14 -1
- package/src/submit-tool.ts +20 -2
- package/src/task-ledger.ts +14 -55
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +97 -34
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1427 -0
- package/src/workflow-state-machine.ts +4478 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +984 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { canonicalJson } from "./canonical.ts";
|
|
3
|
+
|
|
4
|
+
export class SqliteSchemaError extends Error {
|
|
5
|
+
readonly code = "storage-schema-invalid";
|
|
6
|
+
constructor() {
|
|
7
|
+
super("storage-schema-invalid");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Row = Record<string, string | number | null>;
|
|
12
|
+
interface TableContract {
|
|
13
|
+
columns: Row[];
|
|
14
|
+
foreignKeys: string[];
|
|
15
|
+
uniqueKeys: string[];
|
|
16
|
+
strict: number;
|
|
17
|
+
}
|
|
18
|
+
const contracts = new Map<string, Map<string, TableContract>>();
|
|
19
|
+
const identifier = (name: string): string => `"${name.replaceAll('"', '""')}"`;
|
|
20
|
+
|
|
21
|
+
export function configureSqlite(
|
|
22
|
+
database: DatabaseSync,
|
|
23
|
+
busyTimeoutMs = 5000,
|
|
24
|
+
): void {
|
|
25
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
26
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
27
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
28
|
+
database.exec(
|
|
29
|
+
`PRAGMA busy_timeout = ${Math.max(1, Math.min(30_000, busyTimeoutMs))}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function describeTable(database: DatabaseSync, table: string): TableContract {
|
|
34
|
+
const columns = database
|
|
35
|
+
.prepare(`PRAGMA table_info(${identifier(table)})`)
|
|
36
|
+
.all() as Row[];
|
|
37
|
+
const foreignKeys = database
|
|
38
|
+
.prepare(`PRAGMA foreign_key_list(${identifier(table)})`)
|
|
39
|
+
.all() as Row[];
|
|
40
|
+
const indexes = database
|
|
41
|
+
.prepare(`PRAGMA index_list(${identifier(table)})`)
|
|
42
|
+
.all() as Row[];
|
|
43
|
+
const uniqueKeys = indexes
|
|
44
|
+
.filter((row) => row.unique === 1)
|
|
45
|
+
.map((row) => {
|
|
46
|
+
const fields = database
|
|
47
|
+
.prepare(`PRAGMA index_xinfo(${identifier(String(row.name))})`)
|
|
48
|
+
.all() as Row[];
|
|
49
|
+
return canonicalJson({
|
|
50
|
+
partial: row.partial,
|
|
51
|
+
fields: fields
|
|
52
|
+
.filter((field) => field.key === 1)
|
|
53
|
+
.map(({ name, coll, desc }) => ({ name, coll, desc })),
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
.sort();
|
|
57
|
+
const strict = (
|
|
58
|
+
database
|
|
59
|
+
.prepare(
|
|
60
|
+
"SELECT strict FROM pragma_table_list WHERE schema = 'main' AND name = ?",
|
|
61
|
+
)
|
|
62
|
+
.get(table) as Row | undefined
|
|
63
|
+
)?.strict;
|
|
64
|
+
return {
|
|
65
|
+
columns: columns.map(({ name, type, notnull, dflt_value, pk }) => ({
|
|
66
|
+
name,
|
|
67
|
+
type,
|
|
68
|
+
notnull,
|
|
69
|
+
dflt_value,
|
|
70
|
+
pk,
|
|
71
|
+
})),
|
|
72
|
+
foreignKeys: foreignKeys
|
|
73
|
+
.map(({ table: target, from, to, on_update, on_delete, match, seq }) =>
|
|
74
|
+
canonicalJson({ target, from, to, on_update, on_delete, match, seq }),
|
|
75
|
+
)
|
|
76
|
+
.sort(),
|
|
77
|
+
uniqueKeys,
|
|
78
|
+
strict: Number(strict ?? 0),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function schemaContract(schema: string): Map<string, TableContract> {
|
|
83
|
+
const cached = contracts.get(schema);
|
|
84
|
+
if (cached) return cached;
|
|
85
|
+
const reference = new DatabaseSync(":memory:");
|
|
86
|
+
try {
|
|
87
|
+
reference.exec(schema);
|
|
88
|
+
const names = reference
|
|
89
|
+
.prepare(
|
|
90
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
|
91
|
+
)
|
|
92
|
+
.all() as Row[];
|
|
93
|
+
const contract = new Map(
|
|
94
|
+
names.map(({ name }) => [
|
|
95
|
+
String(name),
|
|
96
|
+
describeTable(reference, String(name)),
|
|
97
|
+
]),
|
|
98
|
+
);
|
|
99
|
+
contracts.set(schema, contract);
|
|
100
|
+
return contract;
|
|
101
|
+
} finally {
|
|
102
|
+
reference.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Check every required column, key, foreign key and STRICT boundary; permit additive columns. */
|
|
107
|
+
export function matchesSqliteSchema(
|
|
108
|
+
database: DatabaseSync,
|
|
109
|
+
schema: string,
|
|
110
|
+
): boolean {
|
|
111
|
+
for (const [table, expected] of schemaContract(schema)) {
|
|
112
|
+
const actual = describeTable(database, table);
|
|
113
|
+
if (
|
|
114
|
+
actual.strict !== expected.strict ||
|
|
115
|
+
expected.columns.some(
|
|
116
|
+
(column) =>
|
|
117
|
+
!actual.columns.some(
|
|
118
|
+
(candidate) => canonicalJson(candidate) === canonicalJson(column),
|
|
119
|
+
),
|
|
120
|
+
) ||
|
|
121
|
+
canonicalJson(actual.foreignKeys) !==
|
|
122
|
+
canonicalJson(expected.foreignKeys) ||
|
|
123
|
+
canonicalJson(actual.uniqueKeys) !== canonicalJson(expected.uniqueKeys)
|
|
124
|
+
)
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Serialize creation and the closed, additive migration set across concurrent openers. */
|
|
131
|
+
export function ensureSqliteSchema(
|
|
132
|
+
database: DatabaseSync,
|
|
133
|
+
schema: string,
|
|
134
|
+
additions: Record<string, Record<string, string>> = {},
|
|
135
|
+
): void {
|
|
136
|
+
database.exec("BEGIN IMMEDIATE");
|
|
137
|
+
try {
|
|
138
|
+
const expected = [...schemaContract(schema).keys()];
|
|
139
|
+
const present = expected.filter((table) =>
|
|
140
|
+
database
|
|
141
|
+
.prepare(
|
|
142
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
143
|
+
)
|
|
144
|
+
.get(table),
|
|
145
|
+
);
|
|
146
|
+
// An absent namespace is a first open; a partially missing namespace is damaged.
|
|
147
|
+
if (present.length > 0 && present.length !== expected.length)
|
|
148
|
+
throw new SqliteSchemaError();
|
|
149
|
+
database.exec(schema);
|
|
150
|
+
for (const [table, columns] of Object.entries(additions)) {
|
|
151
|
+
const existing = new Set(
|
|
152
|
+
describeTable(database, table).columns.map(({ name }) => name),
|
|
153
|
+
);
|
|
154
|
+
for (const [column, declaration] of Object.entries(columns)) {
|
|
155
|
+
if (!existing.has(column))
|
|
156
|
+
database.exec(
|
|
157
|
+
`ALTER TABLE ${identifier(table)} ADD COLUMN ${identifier(column)} ${declaration}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!matchesSqliteSchema(database, schema)) throw new SqliteSchemaError();
|
|
162
|
+
database.exec("COMMIT");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
try {
|
|
165
|
+
database.exec("ROLLBACK");
|
|
166
|
+
} catch {
|
|
167
|
+
/* Preserve the original failure. */
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
export const ROUTE_HEALTH_SCHEMA = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS route_health (
|
|
3
|
+
route_fingerprint TEXT PRIMARY KEY,
|
|
4
|
+
state TEXT NOT NULL,
|
|
5
|
+
next_half_open_at INTEGER,
|
|
6
|
+
projection_json TEXT NOT NULL
|
|
7
|
+
) STRICT;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
/** Owner-private storage contracts. Migrations run atomically before use. */
|
|
11
|
+
export const RUN_SCHEMA = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
13
|
+
run_id TEXT PRIMARY KEY,
|
|
14
|
+
root_hash TEXT NOT NULL,
|
|
15
|
+
stage TEXT NOT NULL,
|
|
16
|
+
lookup_key TEXT NOT NULL,
|
|
17
|
+
change_name TEXT,
|
|
18
|
+
provisional_key TEXT,
|
|
19
|
+
state TEXT NOT NULL,
|
|
20
|
+
sequence INTEGER NOT NULL,
|
|
21
|
+
event_hash TEXT NOT NULL,
|
|
22
|
+
delivery_revision INTEGER,
|
|
23
|
+
projection_json TEXT NOT NULL,
|
|
24
|
+
terminal_tombstone TEXT,
|
|
25
|
+
UNIQUE (root_hash, stage, lookup_key)
|
|
26
|
+
) STRICT;
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS delivery_bindings (
|
|
29
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
30
|
+
gate TEXT NOT NULL,
|
|
31
|
+
revision INTEGER NOT NULL,
|
|
32
|
+
receipt_hash TEXT NOT NULL,
|
|
33
|
+
approval_revision INTEGER,
|
|
34
|
+
contract_hash TEXT,
|
|
35
|
+
record_hash TEXT,
|
|
36
|
+
operation_id TEXT NOT NULL,
|
|
37
|
+
PRIMARY KEY (run_id, gate, revision),
|
|
38
|
+
UNIQUE (run_id, operation_id)
|
|
39
|
+
) STRICT;
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
42
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
43
|
+
sequence INTEGER NOT NULL,
|
|
44
|
+
type TEXT NOT NULL,
|
|
45
|
+
payload_json TEXT NOT NULL,
|
|
46
|
+
prior_hash TEXT NOT NULL,
|
|
47
|
+
event_hash TEXT NOT NULL,
|
|
48
|
+
PRIMARY KEY (run_id, sequence),
|
|
49
|
+
UNIQUE (run_id, event_hash)
|
|
50
|
+
) STRICT;
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
53
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
54
|
+
task_id TEXT NOT NULL,
|
|
55
|
+
state TEXT NOT NULL,
|
|
56
|
+
phase TEXT,
|
|
57
|
+
projection_json TEXT NOT NULL,
|
|
58
|
+
PRIMARY KEY (run_id, task_id)
|
|
59
|
+
) STRICT;
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
62
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
63
|
+
operation_id TEXT NOT NULL,
|
|
64
|
+
kind TEXT NOT NULL,
|
|
65
|
+
state TEXT NOT NULL,
|
|
66
|
+
outcome_json TEXT,
|
|
67
|
+
lease_token TEXT,
|
|
68
|
+
lease_expires_at INTEGER,
|
|
69
|
+
PRIMARY KEY (run_id, operation_id)
|
|
70
|
+
) STRICT;
|
|
71
|
+
|
|
72
|
+
${ROUTE_HEALTH_SCHEMA}
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
75
|
+
content_hash TEXT PRIMARY KEY,
|
|
76
|
+
size_bytes INTEGER NOT NULL,
|
|
77
|
+
seal_state TEXT NOT NULL,
|
|
78
|
+
owner_run_id TEXT REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
79
|
+
reference_count INTEGER NOT NULL
|
|
80
|
+
) STRICT;
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS workspace_revisions (
|
|
83
|
+
revision_id TEXT PRIMARY KEY,
|
|
84
|
+
owner_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
85
|
+
parent_revision_id TEXT,
|
|
86
|
+
manifest_hash TEXT NOT NULL
|
|
87
|
+
) STRICT;
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS apply_transactions (
|
|
90
|
+
transaction_id TEXT PRIMARY KEY,
|
|
91
|
+
owner_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
92
|
+
state TEXT NOT NULL,
|
|
93
|
+
intent_json TEXT NOT NULL,
|
|
94
|
+
recovery_json TEXT
|
|
95
|
+
) STRICT;
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS bootstrap_handoffs (
|
|
98
|
+
handoff_id TEXT PRIMARY KEY,
|
|
99
|
+
owner_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
100
|
+
receipt_hash TEXT NOT NULL UNIQUE,
|
|
101
|
+
state TEXT NOT NULL,
|
|
102
|
+
facts_json TEXT NOT NULL
|
|
103
|
+
) STRICT;
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
export const DESIGN_SCHEMA = `
|
|
107
|
+
CREATE TABLE IF NOT EXISTS design_facts (
|
|
108
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
109
|
+
sequence INTEGER NOT NULL,
|
|
110
|
+
kind TEXT NOT NULL,
|
|
111
|
+
identity TEXT NOT NULL,
|
|
112
|
+
payload_json TEXT NOT NULL,
|
|
113
|
+
canonical_hash TEXT NOT NULL,
|
|
114
|
+
prior_record_hash TEXT NOT NULL,
|
|
115
|
+
record_hash TEXT NOT NULL,
|
|
116
|
+
PRIMARY KEY (run_id, sequence),
|
|
117
|
+
UNIQUE (run_id, kind, identity),
|
|
118
|
+
UNIQUE (run_id, record_hash)
|
|
119
|
+
) STRICT;
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS design_operations (
|
|
122
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
123
|
+
operation_id TEXT NOT NULL,
|
|
124
|
+
operation TEXT NOT NULL,
|
|
125
|
+
request_hash TEXT NOT NULL,
|
|
126
|
+
outcome_json TEXT NOT NULL,
|
|
127
|
+
PRIMARY KEY (run_id, operation_id)
|
|
128
|
+
) STRICT;
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS design_compiled_plans (
|
|
131
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
132
|
+
revision INTEGER NOT NULL,
|
|
133
|
+
fact_sequence INTEGER NOT NULL,
|
|
134
|
+
raw_sha256 TEXT NOT NULL,
|
|
135
|
+
canonical_hash TEXT NOT NULL,
|
|
136
|
+
plan_bytes BLOB NOT NULL,
|
|
137
|
+
PRIMARY KEY (run_id, revision),
|
|
138
|
+
UNIQUE (run_id, fact_sequence),
|
|
139
|
+
FOREIGN KEY (run_id, fact_sequence)
|
|
140
|
+
REFERENCES design_facts(run_id, sequence) ON DELETE CASCADE
|
|
141
|
+
) STRICT;
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS design_finalization_leases (
|
|
144
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
145
|
+
operation_id TEXT NOT NULL,
|
|
146
|
+
token TEXT NOT NULL UNIQUE,
|
|
147
|
+
expires_at INTEGER NOT NULL
|
|
148
|
+
) STRICT;
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
export const ENGINE_SCHEMA = `
|
|
152
|
+
CREATE TABLE IF NOT EXISTS workflow_engine_runs (
|
|
153
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
154
|
+
current_revision INTEGER,
|
|
155
|
+
baseline_workspace_revision TEXT,
|
|
156
|
+
current_workspace_revision TEXT,
|
|
157
|
+
cleanup_state TEXT NOT NULL DEFAULT 'none',
|
|
158
|
+
route_id TEXT,
|
|
159
|
+
route_fingerprint TEXT,
|
|
160
|
+
transaction_id TEXT,
|
|
161
|
+
verification_json TEXT,
|
|
162
|
+
delivery_diagnostics_json TEXT,
|
|
163
|
+
next_queue_position INTEGER NOT NULL DEFAULT 1
|
|
164
|
+
) STRICT;
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS workflow_engine_deliveries (
|
|
167
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
168
|
+
revision INTEGER NOT NULL,
|
|
169
|
+
gate TEXT NOT NULL,
|
|
170
|
+
receipt_hash TEXT NOT NULL,
|
|
171
|
+
plan_json TEXT NOT NULL,
|
|
172
|
+
PRIMARY KEY (run_id, revision)
|
|
173
|
+
) STRICT;
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS workflow_engine_tasks (
|
|
176
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
177
|
+
task_id TEXT NOT NULL,
|
|
178
|
+
task_order INTEGER NOT NULL,
|
|
179
|
+
delivery_revision INTEGER NOT NULL,
|
|
180
|
+
plan_json TEXT NOT NULL,
|
|
181
|
+
state TEXT NOT NULL,
|
|
182
|
+
phase TEXT NOT NULL,
|
|
183
|
+
pause_code TEXT,
|
|
184
|
+
context_request_json TEXT,
|
|
185
|
+
attempt_diagnostic_json TEXT,
|
|
186
|
+
route_id TEXT,
|
|
187
|
+
route_fingerprint TEXT,
|
|
188
|
+
queue_position INTEGER,
|
|
189
|
+
PRIMARY KEY (run_id, task_id)
|
|
190
|
+
) STRICT;
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS workflow_engine_operations (
|
|
193
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
194
|
+
operation_id TEXT NOT NULL,
|
|
195
|
+
command TEXT NOT NULL,
|
|
196
|
+
state TEXT NOT NULL,
|
|
197
|
+
lease_token TEXT,
|
|
198
|
+
lease_expires_at INTEGER,
|
|
199
|
+
outcome_json TEXT,
|
|
200
|
+
PRIMARY KEY (run_id, operation_id)
|
|
201
|
+
) STRICT;
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
export const RECOVERY_SCHEMA = `
|
|
205
|
+
CREATE TABLE IF NOT EXISTS workflow_context_reads (
|
|
206
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
207
|
+
task_id TEXT NOT NULL,
|
|
208
|
+
path TEXT NOT NULL,
|
|
209
|
+
PRIMARY KEY (run_id, task_id, path)
|
|
210
|
+
) STRICT;
|
|
211
|
+
|
|
212
|
+
CREATE TABLE IF NOT EXISTS workflow_work_budget (
|
|
213
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
214
|
+
used INTEGER NOT NULL CHECK (used >= 0),
|
|
215
|
+
max_work INTEGER NOT NULL CHECK (max_work > 0 AND used <= max_work),
|
|
216
|
+
phase_high_water INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
hard_limit INTEGER NOT NULL DEFAULT 512,
|
|
218
|
+
recovery_policy INTEGER NOT NULL DEFAULT 0
|
|
219
|
+
) STRICT;
|
|
220
|
+
|
|
221
|
+
CREATE TABLE IF NOT EXISTS workflow_recovery_incidents (
|
|
222
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
223
|
+
incident_key TEXT NOT NULL,
|
|
224
|
+
failures INTEGER NOT NULL CHECK (failures >= 0),
|
|
225
|
+
max_attempts INTEGER NOT NULL CHECK (max_attempts > 0),
|
|
226
|
+
feedback_json TEXT NOT NULL,
|
|
227
|
+
conditions_json TEXT NOT NULL DEFAULT '{}',
|
|
228
|
+
PRIMARY KEY (run_id, incident_key)
|
|
229
|
+
) STRICT;
|
|
230
|
+
|
|
231
|
+
CREATE TABLE IF NOT EXISTS workflow_recovery_events (
|
|
232
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
233
|
+
sequence INTEGER NOT NULL CHECK (sequence > 0),
|
|
234
|
+
incident_key TEXT NOT NULL,
|
|
235
|
+
kind TEXT NOT NULL CHECK (kind IN ('failure', 'resolved', 'repair', 'launch')),
|
|
236
|
+
PRIMARY KEY (run_id, sequence)
|
|
237
|
+
) STRICT;
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
// Independent namespace so existing recovery stores remain atomically additive.
|
|
241
|
+
export const AMENDMENT_SCHEMA = `
|
|
242
|
+
CREATE TABLE IF NOT EXISTS workflow_amendment_budget (
|
|
243
|
+
run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
244
|
+
used INTEGER NOT NULL CHECK (used >= 0 AND used <= 64)
|
|
245
|
+
) STRICT;
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
export const REJECTED_DELIVERY_SCHEMA = `
|
|
249
|
+
CREATE TABLE IF NOT EXISTS workflow_rejected_deliveries (
|
|
250
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
251
|
+
delivery_revision INTEGER NOT NULL CHECK (delivery_revision > 0),
|
|
252
|
+
receipt_hash TEXT NOT NULL,
|
|
253
|
+
PRIMARY KEY (run_id, delivery_revision, receipt_hash)
|
|
254
|
+
) STRICT;
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
export const TASK_SCHEMA = `
|
|
258
|
+
CREATE TABLE IF NOT EXISTS candidates (
|
|
259
|
+
candidate_id TEXT PRIMARY KEY,
|
|
260
|
+
identity_json TEXT NOT NULL,
|
|
261
|
+
state TEXT NOT NULL,
|
|
262
|
+
next_sequence INTEGER NOT NULL,
|
|
263
|
+
total_bytes INTEGER NOT NULL,
|
|
264
|
+
artifact_hash TEXT,
|
|
265
|
+
candidate_hash TEXT,
|
|
266
|
+
segment_count INTEGER,
|
|
267
|
+
paths_json TEXT,
|
|
268
|
+
pause_code TEXT
|
|
269
|
+
) STRICT;
|
|
270
|
+
CREATE TABLE IF NOT EXISTS candidate_segments (
|
|
271
|
+
candidate_id TEXT NOT NULL REFERENCES candidates(candidate_id) ON DELETE CASCADE,
|
|
272
|
+
sequence INTEGER NOT NULL,
|
|
273
|
+
artifact_hash TEXT NOT NULL,
|
|
274
|
+
size_bytes INTEGER NOT NULL,
|
|
275
|
+
segment_hash TEXT NOT NULL,
|
|
276
|
+
PRIMARY KEY (candidate_id, sequence)
|
|
277
|
+
) STRICT;
|
|
278
|
+
CREATE TABLE IF NOT EXISTS task_ledger (
|
|
279
|
+
run_id TEXT NOT NULL,
|
|
280
|
+
task_id TEXT NOT NULL,
|
|
281
|
+
delivery_revision INTEGER NOT NULL,
|
|
282
|
+
boundary_hash TEXT NOT NULL,
|
|
283
|
+
objective TEXT NOT NULL,
|
|
284
|
+
context_refs_json TEXT NOT NULL,
|
|
285
|
+
initial_phase TEXT NOT NULL,
|
|
286
|
+
PRIMARY KEY (run_id, task_id)
|
|
287
|
+
) STRICT;
|
|
288
|
+
CREATE TABLE IF NOT EXISTS task_events (
|
|
289
|
+
run_id TEXT NOT NULL,
|
|
290
|
+
task_id TEXT NOT NULL,
|
|
291
|
+
ordinal INTEGER NOT NULL,
|
|
292
|
+
event_id TEXT NOT NULL,
|
|
293
|
+
event_json TEXT NOT NULL,
|
|
294
|
+
prior_hash TEXT NOT NULL,
|
|
295
|
+
event_hash TEXT NOT NULL,
|
|
296
|
+
PRIMARY KEY (run_id, task_id, ordinal),
|
|
297
|
+
UNIQUE (run_id, task_id, event_id),
|
|
298
|
+
FOREIGN KEY (run_id, task_id) REFERENCES task_ledger(run_id, task_id) ON DELETE CASCADE
|
|
299
|
+
) STRICT;
|
|
300
|
+
CREATE TABLE IF NOT EXISTS durable_facts (
|
|
301
|
+
fact_key TEXT PRIMARY KEY,
|
|
302
|
+
fact_json TEXT NOT NULL
|
|
303
|
+
) STRICT;
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
export const APPLY_SCHEMA = `
|
|
307
|
+
CREATE TABLE IF NOT EXISTS apply_transaction_journal (
|
|
308
|
+
transaction_id TEXT PRIMARY KEY,
|
|
309
|
+
journal_json TEXT NOT NULL,
|
|
310
|
+
journal_hash TEXT NOT NULL
|
|
311
|
+
) STRICT;
|
|
312
|
+
`;
|
|
313
|
+
|
|
314
|
+
export const ENGINE_ADDITIONS = {
|
|
315
|
+
workflow_engine_runs: {
|
|
316
|
+
baseline_workspace_revision: "TEXT",
|
|
317
|
+
current_workspace_revision: "TEXT",
|
|
318
|
+
cleanup_state: "TEXT NOT NULL DEFAULT 'none'",
|
|
319
|
+
verification_json: "TEXT",
|
|
320
|
+
delivery_diagnostics_json: "TEXT",
|
|
321
|
+
route_fingerprint: "TEXT",
|
|
322
|
+
},
|
|
323
|
+
workflow_engine_tasks: {
|
|
324
|
+
route_fingerprint: "TEXT",
|
|
325
|
+
context_request_json: "TEXT",
|
|
326
|
+
attempt_diagnostic_json: "TEXT",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export const RECOVERY_ADDITIONS = {
|
|
331
|
+
workflow_work_budget: {
|
|
332
|
+
phase_high_water: "INTEGER NOT NULL DEFAULT 0",
|
|
333
|
+
hard_limit: "INTEGER NOT NULL DEFAULT 512",
|
|
334
|
+
recovery_policy: "INTEGER NOT NULL DEFAULT 0",
|
|
335
|
+
},
|
|
336
|
+
workflow_recovery_incidents: {
|
|
337
|
+
conditions_json: "TEXT NOT NULL DEFAULT '{}'",
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export const RECOVERY_GRANT_SCHEMA = `
|
|
342
|
+
CREATE TABLE IF NOT EXISTS workflow_recovery_grants (
|
|
343
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
344
|
+
operation_id TEXT NOT NULL,
|
|
345
|
+
incident_key TEXT NOT NULL,
|
|
346
|
+
failure_sequence INTEGER NOT NULL,
|
|
347
|
+
reason TEXT NOT NULL,
|
|
348
|
+
consumed INTEGER NOT NULL DEFAULT 0 CHECK (consumed IN (0, 1)),
|
|
349
|
+
PRIMARY KEY (run_id, operation_id),
|
|
350
|
+
UNIQUE (run_id, incident_key, failure_sequence)
|
|
351
|
+
) STRICT;
|
|
352
|
+
`;
|
package/src/subagent-activity.ts
CHANGED
|
@@ -532,6 +532,17 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
532
532
|
: undefined;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
function hasAutomaticContinuation(result: Record<string, unknown>): boolean {
|
|
536
|
+
const continuation = asRecord(result.continuation);
|
|
537
|
+
return (
|
|
538
|
+
["paused", "approval-needed"].includes(String(result.state)) &&
|
|
539
|
+
asRecord(result.pause)?.code !== "operation-cancelled" &&
|
|
540
|
+
continuation?.owner === "parent" &&
|
|
541
|
+
continuation.automatic === true &&
|
|
542
|
+
(continuation.action === "amend" || continuation.command === "resume")
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
535
546
|
function workflowStateOf(
|
|
536
547
|
result: Record<string, unknown>,
|
|
537
548
|
): WorkflowActivityState {
|
|
@@ -544,6 +555,7 @@ function workflowStateOf(
|
|
|
544
555
|
) {
|
|
545
556
|
return "operation-cancelled";
|
|
546
557
|
}
|
|
558
|
+
if (hasAutomaticContinuation(result)) return "recovering";
|
|
547
559
|
const state = result.state;
|
|
548
560
|
switch (state) {
|
|
549
561
|
case "not-started":
|
|
@@ -755,7 +767,8 @@ export function projectWorkflowActivity(
|
|
|
755
767
|
...(workflowWait(state, payload)
|
|
756
768
|
? { wait: workflowWait(state, payload) }
|
|
757
769
|
: {}),
|
|
758
|
-
...(
|
|
770
|
+
...(!hasAutomaticContinuation(payload) &&
|
|
771
|
+
nextWorkflowAction(state, legalCommands)
|
|
759
772
|
? { nextAction: nextWorkflowAction(state, legalCommands) }
|
|
760
773
|
: {}),
|
|
761
774
|
};
|
package/src/submit-tool.ts
CHANGED
|
@@ -225,6 +225,7 @@ export interface CandidateContextBoundary {
|
|
|
225
225
|
writePaths: readonly string[];
|
|
226
226
|
deletePaths?: readonly string[];
|
|
227
227
|
taskPaths: readonly string[];
|
|
228
|
+
contextReadRoots?: readonly string[];
|
|
228
229
|
redWritePaths: readonly string[];
|
|
229
230
|
agents: {
|
|
230
231
|
impact: "none" | "update-existing" | "create-index" | "remove-index";
|
|
@@ -354,6 +355,19 @@ function pathWithinAny(relative: string, approved: readonly string[]): boolean {
|
|
|
354
355
|
return approved.some((path) => contextRefWithinBoundary(relative, path));
|
|
355
356
|
}
|
|
356
357
|
|
|
358
|
+
export function permitsContextRead(
|
|
359
|
+
relative: string,
|
|
360
|
+
roots: readonly string[],
|
|
361
|
+
): boolean {
|
|
362
|
+
// Dynamic discovery never grants hidden credentials or a directory tree.
|
|
363
|
+
return (
|
|
364
|
+
isValidRelativePath(relative) &&
|
|
365
|
+
relative.split("/").every((part) => !part.startsWith(".")) &&
|
|
366
|
+
!/\.(?:pem|key|p12|pfx)$/iu.test(relative) &&
|
|
367
|
+
pathWithinAny(relative, roots)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
357
371
|
export function classifyCandidateContextRequest(
|
|
358
372
|
request: {
|
|
359
373
|
kind: "context-request";
|
|
@@ -377,11 +391,15 @@ export function classifyCandidateContextRequest(
|
|
|
377
391
|
const ordinaryRequested = requested.filter((ref) => !isAgentsPath(ref.path));
|
|
378
392
|
const outsideOrdinary = ordinaryRequested.filter(
|
|
379
393
|
(ref) =>
|
|
394
|
+
!(
|
|
395
|
+
ref.access === "read" &&
|
|
396
|
+
permitsContextRead(ref.path, boundary.contextReadRoots ?? [])
|
|
397
|
+
) &&
|
|
380
398
|
!pathWithinAny(
|
|
381
399
|
ref.path,
|
|
382
400
|
ref.access === "write"
|
|
383
401
|
? [...boundary.writePaths, ...(boundary.deletePaths ?? [])]
|
|
384
|
-
: phasePaths,
|
|
402
|
+
: [...phasePaths, ...boundary.taskPaths],
|
|
385
403
|
),
|
|
386
404
|
);
|
|
387
405
|
const agentsRequested = requested.filter((ref) => isAgentsPath(ref.path));
|
|
@@ -428,7 +446,7 @@ export function classifyCandidateContextRequest(
|
|
|
428
446
|
classification = { kind: "paused", code: "agents-context-needed" };
|
|
429
447
|
} else {
|
|
430
448
|
classification = {
|
|
431
|
-
kind:
|
|
449
|
+
kind: "retryable",
|
|
432
450
|
code:
|
|
433
451
|
request.code === "task-split-needed"
|
|
434
452
|
? "task-split-needed"
|