@abelxiaoxing/cadence 1.0.1 → 1.0.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/src/scheduler.ts CHANGED
@@ -59,6 +59,7 @@ function conflictEdgeMatches(
59
59
  ): boolean {
60
60
  return conflicts.some(
61
61
  (edge) =>
62
+ edge === other.taskId ||
62
63
  edge === other.id ||
63
64
  other.declared.read.includes(edge) ||
64
65
  other.declared.write.includes(edge),
@@ -31,6 +31,7 @@ export interface ScopedToolDef {
31
31
 
32
32
  interface Options {
33
33
  roots: string[];
34
+ allowedPaths?: string[];
34
35
  observer?: (obs: Observation) => void;
35
36
  }
36
37
 
@@ -38,7 +39,11 @@ type PathResult =
38
39
  | { ok: true; abs: string; rel: string }
39
40
  | { ok: false; error: string };
40
41
 
41
- function resolveScoped(roots: string[], input: unknown): PathResult {
42
+ function resolveScoped(
43
+ roots: string[],
44
+ input: unknown,
45
+ allowedPaths?: string[],
46
+ ): PathResult {
42
47
  if (typeof input !== "string" || input.length === 0) {
43
48
  return { ok: false, error: "missing path" };
44
49
  }
@@ -54,11 +59,21 @@ function resolveScoped(roots: string[], input: unknown): PathResult {
54
59
  if (input.startsWith("./") || input.includes("//") || input.endsWith("/.")) {
55
60
  return { ok: false, error: "noncanonical path rejected" };
56
61
  }
62
+ let insideRoot = false;
57
63
  for (const root of roots) {
58
64
  const abs = resolve(root, input);
59
65
  const inside =
60
66
  abs === root || abs.startsWith(root.endsWith("/") ? root : `${root}/`);
61
67
  if (!inside) continue;
68
+ insideRoot = true;
69
+ if (
70
+ allowedPaths &&
71
+ !allowedPaths.some(
72
+ (allowed) => abs === allowed || abs.startsWith(`${allowed}/`),
73
+ )
74
+ ) {
75
+ continue;
76
+ }
62
77
  // Hidden directories are rejected unless the root itself is the hidden scope.
63
78
  const hiddenRoot =
64
79
  basename(realpathSync(root)) === ".git" ||
@@ -89,7 +104,12 @@ function resolveScoped(roots: string[], input: unknown): PathResult {
89
104
  }
90
105
  return { ok: true, abs: abs, rel: relative(root, abs) || "." };
91
106
  }
92
- return { ok: false, error: "path is outside every approved root" };
107
+ return {
108
+ ok: false,
109
+ error: insideRoot
110
+ ? "path is outside the declared read/write scope"
111
+ : "path is outside every approved root",
112
+ };
93
113
  }
94
114
 
95
115
  function existsSync(p: string): boolean {
@@ -113,6 +133,7 @@ function utf8Text(abs: string): string | null {
113
133
 
114
134
  export function createScopedTools(opts: Options): ScopedToolDef[] {
115
135
  const roots = opts.roots.map((r) => resolve(r));
136
+ const allowedPaths = opts.allowedPaths?.map((p) => resolve(p));
116
137
  const observe = opts.observer ?? (() => {});
117
138
 
118
139
  const readTool: ScopedToolDef = {
@@ -120,7 +141,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
120
141
  description:
121
142
  "Read a regular UTF-8 text file within the approved scope (max 2,000 lines / 50 KiB).",
122
143
  async execute(params) {
123
- const resolved = resolveScoped(roots, params.path);
144
+ const resolved = resolveScoped(roots, params.path, allowedPaths);
124
145
  if (!resolved.ok) return { ok: false, error: resolved.error };
125
146
  let st: Stats | undefined;
126
147
  try {
@@ -167,7 +188,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
167
188
  error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
168
189
  };
169
190
  }
170
- const resolved = resolveScoped(roots, params.path ?? ".");
191
+ const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
171
192
  if (!resolved.ok) return { ok: false, error: resolved.error };
172
193
  let regex: RegExp;
173
194
  try {
@@ -207,7 +228,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
207
228
  description:
208
229
  "List a directory within the approved scope (max 20,000 entries, stable code-unit order).",
209
230
  async execute(params) {
210
- const resolved = resolveScoped(roots, params.path ?? ".");
231
+ const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
211
232
  if (!resolved.ok) return { ok: false, error: resolved.error };
212
233
  let st: Stats | undefined;
213
234
  try {
@@ -236,7 +257,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
236
257
  description:
237
258
  "Recursively list paths within the approved scope (max 20,000 entries, stable code-unit order).",
238
259
  async execute(params) {
239
- const resolved = resolveScoped(roots, params.path ?? ".");
260
+ const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
240
261
  if (!resolved.ok) return { ok: false, error: resolved.error };
241
262
  let st: Stats | undefined;
242
263
  try {
@@ -63,6 +63,7 @@ export interface SubmitClassification {
63
63
 
64
64
  export function createSubmitTool(input: {
65
65
  requestId: string;
66
+ taskId?: string;
66
67
  role: string;
67
68
  phase: string;
68
69
  output: "evidence" | "diff";
@@ -98,7 +99,8 @@ export function createSubmitTool(input: {
98
99
  if (value.id !== input.requestId) identity.request = false;
99
100
  if (value.role !== input.role) identity.role = false;
100
101
  if (input.output === "diff") {
101
- if (value.taskId !== input.requestId) identity.task = false;
102
+ if (value.taskId !== (input.taskId ?? input.requestId))
103
+ identity.task = false;
102
104
  if (value.phase !== input.phase) identity.phase = false;
103
105
  }
104
106
  const validation =
package/src/worker.ts CHANGED
@@ -1,77 +1,149 @@
1
1
  // Process-local logical Worker registry for private orchestration. A Worker
2
- // is pinned by the first accepted phase: logical ID, stage, role, the
3
- // provider/model/API/thinking identity, and the approved non-snapshot
4
- // contract. Later accepted Red/Green/optional Refactor phases receive fresh
5
- // snapshots and fresh phase-local auth for the SAME pinned identity. A failed
6
- // phase may be mechanically redispatched exactly once when its canonical
7
- // request is identical apart from refreshed snapshot hashes; a second failure
8
- // blocks that branch. No credential, transcript, or retry ledger is retained.
2
+ // is keyed by stable task identity. Its immutable task contract is separate
3
+ // from the current phase request, snapshot, verification, and launch budget.
4
+ // No credential, transcript, provider diagnostic, or unbounded retry ledger
5
+ // is retained.
9
6
 
10
- import type { RequestEnvelope } from "./contracts.ts";
7
+ import type { RequestEnvelope, VerificationContract } from "./contracts.ts";
11
8
 
12
9
  export interface WorkerContract {
13
10
  stage: string;
14
11
  role: string;
15
- id: string;
12
+ taskId: string;
16
13
  objective: string;
17
14
  roots: string[];
18
15
  agents: string;
19
16
  contractText: string;
20
17
  read: string[];
21
- write: string[];
22
18
  conflicts: string[];
23
19
  resources: string[];
24
- verificationLock?: string;
25
20
  output: string;
21
+ currentPhase: WorkerPhaseContract;
22
+ }
23
+
24
+ export interface WorkerTaskContract {
25
+ stage: string;
26
+ role: string;
27
+ taskId: string;
28
+ objective: string;
29
+ roots: string[];
30
+ agents: string;
31
+ contractText: string;
32
+ read: string[];
33
+ conflicts: string[];
34
+ resources: string[];
35
+ output: string;
36
+ }
37
+
38
+ export interface WorkerPhaseContract {
39
+ requestId: string;
40
+ phase: string;
41
+ readSet: string[];
42
+ writeSet: string[];
43
+ conflicts: string[];
44
+ resources: string[];
45
+ verificationLock?: string;
46
+ snapshot?: unknown;
47
+ verification?: VerificationContract;
48
+ correctionIndex: 0 | 1;
26
49
  }
27
50
 
28
51
  export interface LogicalWorker {
29
52
  identity: string;
30
- contract: WorkerContract;
31
- redispatchUsed: boolean;
53
+ taskContract: WorkerTaskContract;
54
+ currentPhase: WorkerPhaseContract;
55
+ state: WorkerState;
32
56
  }
33
57
 
34
- // Pins the semantic scope of a logical Worker, excluding the phase label so a
35
- // later Red/Green/Refactor phase is recognized as the same Worker.
58
+ export type WorkerState =
59
+ | { kind: "ready" }
60
+ | { kind: "candidate-pending" }
61
+ | { kind: "phase-applied" }
62
+ | { kind: "artifact-correction-pending"; rejection: string }
63
+ | { kind: "stale-redispatch-pending" }
64
+ | { kind: "blocked"; reason: "artifact" | "mechanical" };
65
+
36
66
  export function contractOf(envelope: RequestEnvelope): WorkerContract {
37
67
  return {
38
68
  stage: envelope.stage,
39
69
  role: envelope.role,
40
- id: envelope.id,
70
+ taskId: envelope.taskId ?? envelope.id,
41
71
  objective: envelope.objective,
42
72
  roots: [...envelope.roots],
43
73
  agents: envelope.context.agents,
44
74
  contractText: envelope.context.contract,
45
75
  read: [...envelope.declared.read],
46
- write: [...envelope.declared.write],
47
76
  conflicts: [...envelope.declared.conflicts],
48
77
  resources: [...envelope.declared.resources],
49
- verificationLock: envelope.declared.verificationLock,
50
78
  output: envelope.output,
79
+ currentPhase: phaseOf(envelope, 0),
51
80
  };
52
81
  }
53
82
 
54
- export function sameContract(a: WorkerContract, b: WorkerContract): boolean {
83
+ export function sameContract(
84
+ a: WorkerTaskContract,
85
+ b: WorkerTaskContract,
86
+ ): boolean {
55
87
  if (
56
88
  a.stage !== b.stage ||
57
89
  a.role !== b.role ||
58
- a.id !== b.id ||
90
+ a.taskId !== b.taskId ||
59
91
  a.objective !== b.objective ||
60
92
  a.agents !== b.agents ||
61
93
  a.contractText !== b.contractText ||
62
- a.output !== b.output ||
63
- a.verificationLock !== b.verificationLock
94
+ a.output !== b.output
64
95
  )
65
96
  return false;
66
97
  return (
67
98
  arrayEqual(a.roots, b.roots) &&
68
99
  arrayEqual(a.read, b.read) &&
69
- arrayEqual(a.write, b.write) &&
70
100
  arrayEqual(a.conflicts, b.conflicts) &&
71
101
  arrayEqual(a.resources, b.resources)
72
102
  );
73
103
  }
74
104
 
105
+ export function samePhaseContract(
106
+ a: WorkerPhaseContract,
107
+ b: WorkerPhaseContract,
108
+ ): boolean {
109
+ return (
110
+ a.phase === b.phase &&
111
+ arrayEqual(a.readSet, b.readSet) &&
112
+ arrayEqual(a.writeSet, b.writeSet) &&
113
+ arrayEqual(a.conflicts, b.conflicts) &&
114
+ arrayEqual(a.resources, b.resources) &&
115
+ a.verificationLock === b.verificationLock &&
116
+ JSON.stringify(a.verification) === JSON.stringify(b.verification)
117
+ );
118
+ }
119
+
120
+ function phaseOf(
121
+ envelope: RequestEnvelope,
122
+ correctionIndex: 0 | 1,
123
+ ): WorkerPhaseContract {
124
+ return {
125
+ requestId: envelope.id,
126
+ phase: envelope.phase,
127
+ readSet: [...envelope.declared.read],
128
+ writeSet: [...envelope.declared.write],
129
+ conflicts: [...envelope.declared.conflicts],
130
+ resources: [...envelope.declared.resources],
131
+ verificationLock: envelope.declared.verificationLock,
132
+ snapshot:
133
+ envelope.snapshot === undefined
134
+ ? undefined
135
+ : structuredClone(envelope.snapshot),
136
+ verification:
137
+ envelope.verification === undefined
138
+ ? undefined
139
+ : {
140
+ ...envelope.verification,
141
+ argv: [...envelope.verification.argv],
142
+ },
143
+ correctionIndex,
144
+ };
145
+ }
146
+
75
147
  function arrayEqual(a: string[], b: string[]): boolean {
76
148
  return a.length === b.length && a.every((value, index) => value === b[index]);
77
149
  }
@@ -104,12 +176,30 @@ export class WorkerRegistry {
104
176
  }
105
177
 
106
178
  pin(contract: WorkerContract, identity: string): LogicalWorker {
179
+ const currentPhase = clonePhase(contract.currentPhase);
107
180
  const worker: LogicalWorker = {
108
181
  identity,
109
- contract,
110
- redispatchUsed: false,
182
+ taskContract: cloneTask(contract),
183
+ currentPhase,
184
+ state: { kind: "ready" },
111
185
  };
112
- this.workers.set(contract.id, worker);
186
+ this.workers.set(contract.taskId, worker);
187
+ return worker;
188
+ }
189
+
190
+ setCurrentPhase(
191
+ taskId: string,
192
+ contract: WorkerContract,
193
+ correctionIndex: 0 | 1,
194
+ ): LogicalWorker | undefined {
195
+ const worker = this.workers.get(taskId);
196
+ if (!worker) return undefined;
197
+ const currentPhase = {
198
+ ...clonePhase(contract.currentPhase),
199
+ correctionIndex,
200
+ };
201
+ worker.currentPhase = currentPhase;
202
+ worker.state = { kind: "ready" };
113
203
  return worker;
114
204
  }
115
205
 
@@ -117,3 +207,23 @@ export class WorkerRegistry {
117
207
  this.workers.clear();
118
208
  }
119
209
  }
210
+
211
+ function cloneTask(contract: WorkerContract): WorkerTaskContract {
212
+ return {
213
+ stage: contract.stage,
214
+ role: contract.role,
215
+ taskId: contract.taskId,
216
+ objective: contract.objective,
217
+ roots: [...contract.roots],
218
+ agents: contract.agents,
219
+ contractText: contract.contractText,
220
+ read: [...contract.read],
221
+ conflicts: [...contract.conflicts],
222
+ resources: [...contract.resources],
223
+ output: contract.output,
224
+ };
225
+ }
226
+
227
+ function clonePhase(phase: WorkerPhaseContract): WorkerPhaseContract {
228
+ return structuredClone(phase);
229
+ }