@abelxiaoxing/cadence 1.0.14 → 1.1.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.
Files changed (46) hide show
  1. package/README.md +29 -13
  2. package/agents/contract-reviewer.md +2 -2
  3. package/agents/design-explorer.md +1 -1
  4. package/agents/diagnosis-worker.md +6 -3
  5. package/agents/implementation-worker.md +14 -9
  6. package/config/.env.example +5 -17
  7. package/config/routes.example.json +42 -0
  8. package/package.json +3 -3
  9. package/prompts/abel-design.md +150 -228
  10. package/prompts/abel-diagnose.md +37 -18
  11. package/prompts/abel-implement.md +111 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +156 -168
  14. package/src/apply-transaction.ts +1427 -0
  15. package/src/artifact-store.ts +204 -0
  16. package/src/child-session.ts +136 -17
  17. package/src/contracts.ts +105 -696
  18. package/src/control-contracts.ts +259 -0
  19. package/src/delivery-compiler.ts +1088 -0
  20. package/src/file-snapshot.ts +5 -0
  21. package/src/implement-graph.ts +11 -4
  22. package/src/index.ts +1465 -133
  23. package/src/isolation-backend.ts +269 -0
  24. package/src/packet-runtime.ts +533 -0
  25. package/src/parent-provider.ts +23 -4
  26. package/src/route-policy.ts +483 -0
  27. package/src/run-state.ts +393 -0
  28. package/src/run-store.ts +918 -0
  29. package/src/safe-path.ts +11 -0
  30. package/src/scoped-tools.ts +36 -0
  31. package/src/state-root.ts +215 -0
  32. package/src/subagent-activity.ts +526 -27
  33. package/src/submit-tool.ts +274 -0
  34. package/src/task-ledger.ts +1380 -0
  35. package/src/verification-capability.ts +529 -42
  36. package/src/worker-broker.ts +703 -0
  37. package/src/workflow-engine.ts +7391 -0
  38. package/src/workspace-store.ts +544 -0
  39. package/src/candidate-preflight.ts +0 -1618
  40. package/src/drain.ts +0 -20
  41. package/src/patch.ts +0 -505
  42. package/src/result-store.ts +0 -312
  43. package/src/runtime.ts +0 -2436
  44. package/src/scheduler.ts +0 -384
  45. package/src/subagent-endpoint.ts +0 -158
  46. package/src/worker.ts +0 -204
package/src/safe-path.ts CHANGED
@@ -64,3 +64,14 @@ export function observeSafePath(
64
64
  export function isSafeRegularFile(root: string, relative: string): boolean {
65
65
  return observeSafePath(root, relative).kind === "file";
66
66
  }
67
+
68
+ /** Require an exact regular-file or absent workspace fact without following links. */
69
+ export function requireSafeWorkspacePath(
70
+ root: string,
71
+ relative: string,
72
+ expected: "file" | "absent",
73
+ ): SafePathObservation {
74
+ const observation = observeSafePath(root, relative);
75
+ if (observation.kind !== expected) throw new Error("unsafe-workspace-path");
76
+ return observation;
77
+ }
@@ -132,6 +132,42 @@ export type Observation =
132
132
  | { kind: "file"; path: string }
133
133
  | { kind: "dir"; path: string };
134
134
 
135
+ export class ScopedObservationCollector {
136
+ readonly #observations = new Map<string, Observation>();
137
+ readonly #limit: number;
138
+ #truncated = false;
139
+
140
+ constructor(limit = 512) {
141
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 4096) {
142
+ throw new Error("observation-limit-invalid");
143
+ }
144
+ this.#limit = limit;
145
+ }
146
+
147
+ readonly observe = (observation: Observation): void => {
148
+ const key = `${observation.kind}:${observation.path}`;
149
+ if (this.#observations.has(key)) return;
150
+ if (this.#observations.size >= this.#limit) {
151
+ this.#truncated = true;
152
+ return;
153
+ }
154
+ this.#observations.set(key, { ...observation });
155
+ };
156
+
157
+ projection(): { observations: Observation[]; truncated: boolean } {
158
+ return {
159
+ observations: [...this.#observations.values()]
160
+ .map((entry) => ({ ...entry }))
161
+ .sort(
162
+ (left, right) =>
163
+ left.path.localeCompare(right.path) ||
164
+ left.kind.localeCompare(right.kind),
165
+ ),
166
+ truncated: this.#truncated,
167
+ };
168
+ }
169
+ }
170
+
135
171
  export interface ScopedToolDef {
136
172
  name: string;
137
173
  description: string;
@@ -0,0 +1,215 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ export const STATE_ROOT_ERROR_CODES = [
7
+ "consumer-root-unavailable",
8
+ "invalid-state-base",
9
+ "state-component-symlink",
10
+ "state-component-type",
11
+ "state-root-inside-consumer",
12
+ ] as const;
13
+
14
+ export type StateRootErrorCode = (typeof STATE_ROOT_ERROR_CODES)[number];
15
+
16
+ export class StateRootError extends Error {
17
+ readonly code: StateRootErrorCode;
18
+
19
+ constructor(code: StateRootErrorCode) {
20
+ super(code);
21
+ this.name = "StateRootError";
22
+ this.code = code;
23
+ }
24
+ }
25
+
26
+ export interface StateRootOptions {
27
+ consumerRoot: string;
28
+ xdgStateHome?: string;
29
+ homeDir?: string;
30
+ }
31
+
32
+ export interface ResolvedStateRoot {
33
+ consumerRoot: string;
34
+ consumerRootHash: string;
35
+ stateBase: string;
36
+ productRoot: string;
37
+ rootsDir: string;
38
+ rootDir: string;
39
+ databasePath: string;
40
+ artifactsDir: string;
41
+ workspacesDir: string;
42
+ transactionsDir: string;
43
+ }
44
+
45
+ function isContainedBy(parent: string, candidate: string): boolean {
46
+ const relative = path.relative(parent, candidate);
47
+ return (
48
+ relative === "" ||
49
+ (!relative.startsWith("..") && !path.isAbsolute(relative))
50
+ );
51
+ }
52
+
53
+ function inspectExistingComponents(absolutePath: string): void {
54
+ const parsed = path.parse(absolutePath);
55
+ const relativeParts = absolutePath
56
+ .slice(parsed.root.length)
57
+ .split(path.sep)
58
+ .filter(Boolean);
59
+ let current = parsed.root;
60
+ for (const part of relativeParts) {
61
+ current = path.join(current, part);
62
+ let stat: ReturnType<typeof lstatSync>;
63
+ try {
64
+ stat = lstatSync(current);
65
+ } catch (error) {
66
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
67
+ throw error;
68
+ }
69
+ if (stat.isSymbolicLink()) {
70
+ throw new StateRootError("state-component-symlink");
71
+ }
72
+ if (!stat.isDirectory() && current !== absolutePath) {
73
+ throw new StateRootError("state-component-type");
74
+ }
75
+ }
76
+ }
77
+
78
+ function canonicalFuturePath(input: string): string {
79
+ const absolute = path.resolve(input);
80
+ inspectExistingComponents(absolute);
81
+ let cursor = absolute;
82
+ const missing: string[] = [];
83
+ for (;;) {
84
+ try {
85
+ return path.join(realpathSync.native(cursor), ...missing.reverse());
86
+ } catch (error) {
87
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
88
+ const parent = path.dirname(cursor);
89
+ if (parent === cursor) throw error;
90
+ missing.push(path.basename(cursor));
91
+ cursor = parent;
92
+ }
93
+ }
94
+ }
95
+
96
+ function requireAbsoluteDirectoryBase(
97
+ xdgStateHome: string | undefined,
98
+ homeDir: string,
99
+ ): string {
100
+ if (xdgStateHome && path.isAbsolute(xdgStateHome)) {
101
+ return canonicalFuturePath(xdgStateHome);
102
+ }
103
+ if (!path.isAbsolute(homeDir)) {
104
+ throw new StateRootError("invalid-state-base");
105
+ }
106
+ return canonicalFuturePath(path.join(homeDir, ".local", "state"));
107
+ }
108
+
109
+ export function resolveStateRoot(options: StateRootOptions): ResolvedStateRoot {
110
+ let consumerRoot: string;
111
+ try {
112
+ consumerRoot = realpathSync.native(path.resolve(options.consumerRoot));
113
+ if (!lstatSync(consumerRoot).isDirectory()) {
114
+ throw new StateRootError("consumer-root-unavailable");
115
+ }
116
+ } catch (error) {
117
+ if (error instanceof StateRootError) throw error;
118
+ throw new StateRootError("consumer-root-unavailable");
119
+ }
120
+
121
+ const stateBase = requireAbsoluteDirectoryBase(
122
+ options.xdgStateHome,
123
+ options.homeDir ?? os.homedir(),
124
+ );
125
+ const consumerRootHash = createHash("sha256")
126
+ .update(consumerRoot)
127
+ .digest("hex");
128
+ const productRoot = canonicalFuturePath(path.join(stateBase, "abel-cadence"));
129
+ const rootsDir = canonicalFuturePath(path.join(productRoot, "roots"));
130
+ const rootDir = canonicalFuturePath(path.join(rootsDir, consumerRootHash));
131
+
132
+ if (isContainedBy(consumerRoot, rootDir)) {
133
+ throw new StateRootError("state-root-inside-consumer");
134
+ }
135
+
136
+ return {
137
+ consumerRoot,
138
+ consumerRootHash,
139
+ stateBase,
140
+ productRoot,
141
+ rootsDir,
142
+ rootDir,
143
+ databasePath: path.join(rootDir, "control.sqlite3"),
144
+ artifactsDir: path.join(rootDir, "artifacts"),
145
+ workspacesDir: path.join(rootDir, "workspaces"),
146
+ transactionsDir: path.join(rootDir, "transactions"),
147
+ };
148
+ }
149
+
150
+ function ensureDirectory(directory: string): void {
151
+ let stat: ReturnType<typeof lstatSync>;
152
+ try {
153
+ stat = lstatSync(directory);
154
+ } catch (error) {
155
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
156
+ const parent = path.dirname(directory);
157
+ if (parent !== directory) ensureDirectory(parent);
158
+ mkdirSync(directory, { mode: 0o700 });
159
+ chmodSync(directory, 0o700);
160
+ return;
161
+ }
162
+ if (stat.isSymbolicLink()) {
163
+ throw new StateRootError("state-component-symlink");
164
+ }
165
+ if (!stat.isDirectory()) {
166
+ throw new StateRootError("state-component-type");
167
+ }
168
+ }
169
+
170
+ export function prepareStateRoot(
171
+ resolved: ResolvedStateRoot,
172
+ ): ResolvedStateRoot {
173
+ const checked = resolveStateRoot({
174
+ consumerRoot: resolved.consumerRoot,
175
+ xdgStateHome: resolved.stateBase,
176
+ homeDir: path.dirname(resolved.stateBase),
177
+ });
178
+ if (
179
+ checked.consumerRootHash !== resolved.consumerRootHash ||
180
+ checked.rootDir !== resolved.rootDir
181
+ ) {
182
+ throw new StateRootError("invalid-state-base");
183
+ }
184
+
185
+ ensureDirectory(resolved.stateBase);
186
+ for (const directory of [
187
+ resolved.productRoot,
188
+ resolved.rootsDir,
189
+ resolved.rootDir,
190
+ resolved.artifactsDir,
191
+ resolved.workspacesDir,
192
+ resolved.transactionsDir,
193
+ ]) {
194
+ ensureDirectory(directory);
195
+ chmodSync(directory, 0o700);
196
+ }
197
+
198
+ try {
199
+ const database = lstatSync(resolved.databasePath);
200
+ if (database.isSymbolicLink()) {
201
+ throw new StateRootError("state-component-symlink");
202
+ }
203
+ if (!database.isFile()) {
204
+ throw new StateRootError("state-component-type");
205
+ }
206
+ } catch (error) {
207
+ if (
208
+ error instanceof StateRootError ||
209
+ (error as NodeJS.ErrnoException).code !== "ENOENT"
210
+ ) {
211
+ throw error;
212
+ }
213
+ }
214
+ return { ...resolved };
215
+ }