@brainervirus/workit-cli 1.0.12 → 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.
- package/dist/index.js +2018 -1626
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -50732,7 +50732,13 @@ var refSchema = discriminatedUnion("kind", [
|
|
|
50732
50732
|
id
|
|
50733
50733
|
}).strict(),
|
|
50734
50734
|
object({ kind: literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
|
|
50735
|
-
object({ kind: literal("external"), url: text.url() }).strict()
|
|
50735
|
+
object({ kind: literal("external"), url: text.url() }).strict(),
|
|
50736
|
+
object({
|
|
50737
|
+
kind: literal("standing"),
|
|
50738
|
+
workspace: nonEmpty,
|
|
50739
|
+
class: nonEmpty,
|
|
50740
|
+
configDigest: nullableDigest
|
|
50741
|
+
}).strict()
|
|
50736
50742
|
]);
|
|
50737
50743
|
var rewriteUnknown = (value, map) => {
|
|
50738
50744
|
if (Array.isArray(value)) {
|
|
@@ -51037,7 +51043,8 @@ var decisionSchema = object({
|
|
|
51037
51043
|
approvedContent: text,
|
|
51038
51044
|
displayed: text.optional(),
|
|
51039
51045
|
contentRefs: array(refSchema),
|
|
51040
|
-
statedChoice: object({ ref: text, text }).strict().optional()
|
|
51046
|
+
statedChoice: object({ ref: text, text }).strict().optional(),
|
|
51047
|
+
standing: object({ workspace: nonEmpty, class: nonEmpty }).strict().optional()
|
|
51041
51048
|
}).strict(),
|
|
51042
51049
|
digest,
|
|
51043
51050
|
response: _enum(["approved", "rejected", "stated"]),
|
|
@@ -56736,7 +56743,7 @@ async function runCutoverCommand(argv, deps = {}) {
|
|
|
56736
56743
|
}
|
|
56737
56744
|
|
|
56738
56745
|
// packages/workit-cli/src/task.ts
|
|
56739
|
-
import { readFileSync as
|
|
56746
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
56740
56747
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
56741
56748
|
|
|
56742
56749
|
// packages/workit-core/src/core.ts
|
|
@@ -58285,1727 +58292,1967 @@ function assertProductWriteAllowed(input) {
|
|
|
58285
58292
|
}
|
|
58286
58293
|
|
|
58287
58294
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
58295
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
58296
|
+
import * as fs10 from "node:fs";
|
|
58297
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
58298
|
+
import path23 from "node:path";
|
|
58299
|
+
|
|
58300
|
+
// packages/workit-core/src/core/authority.ts
|
|
58288
58301
|
import { createHash as createHash7 } from "node:crypto";
|
|
58289
58302
|
import * as fs9 from "node:fs";
|
|
58290
|
-
import
|
|
58291
|
-
import path21 from "node:path";
|
|
58303
|
+
import path22 from "node:path";
|
|
58292
58304
|
|
|
58293
|
-
// packages/workit-core/src/core/
|
|
58305
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
58294
58306
|
import { createHash as createHash6 } from "node:crypto";
|
|
58295
|
-
import
|
|
58296
|
-
|
|
58297
|
-
|
|
58298
|
-
|
|
58299
|
-
|
|
58300
|
-
|
|
58301
|
-
|
|
58307
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
58308
|
+
|
|
58309
|
+
// packages/workit-core/src/core/branch.ts
|
|
58310
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
58311
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
58312
|
+
import path21 from "node:path";
|
|
58313
|
+
|
|
58314
|
+
// packages/workit-core/src/core/git.ts
|
|
58315
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
58316
|
+
var run = (cwd, args) => {
|
|
58317
|
+
try {
|
|
58318
|
+
const stdout = execFileSync2("git", args, {
|
|
58319
|
+
cwd,
|
|
58320
|
+
encoding: "utf8",
|
|
58321
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
58322
|
+
});
|
|
58323
|
+
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
58324
|
+
} catch (error) {
|
|
58325
|
+
const e = error;
|
|
58326
|
+
return {
|
|
58327
|
+
stdout: (e.stdout ?? "").trimEnd(),
|
|
58328
|
+
stderr: (e.stderr ?? "").trimEnd(),
|
|
58329
|
+
exitCode: e.status ?? 1
|
|
58330
|
+
};
|
|
58331
|
+
}
|
|
58302
58332
|
};
|
|
58303
|
-
var
|
|
58304
|
-
|
|
58305
|
-
|
|
58306
|
-
|
|
58307
|
-
|
|
58308
|
-
|
|
58309
|
-
|
|
58310
|
-
if (typeof step === "string") {
|
|
58311
|
-
if (!step.trim())
|
|
58312
|
-
return null;
|
|
58313
|
-
normalized.push({ kind: "commit", message: step });
|
|
58314
|
-
continue;
|
|
58315
|
-
}
|
|
58316
|
-
if (!isRecord4(step))
|
|
58317
|
-
return null;
|
|
58318
|
-
if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
|
|
58319
|
-
normalized.push({ kind: "branch", target: step.branch });
|
|
58320
|
-
continue;
|
|
58333
|
+
var gitContext = (workspaceRoot, paths = []) => {
|
|
58334
|
+
const cwd = workspaceRoot;
|
|
58335
|
+
let failure;
|
|
58336
|
+
const runHere = (args) => {
|
|
58337
|
+
const result = run(cwd, args);
|
|
58338
|
+
if (!failure && result.exitCode !== 0) {
|
|
58339
|
+
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
58321
58340
|
}
|
|
58322
|
-
|
|
58323
|
-
|
|
58324
|
-
|
|
58341
|
+
return result.stdout;
|
|
58342
|
+
};
|
|
58343
|
+
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
58344
|
+
const status_short = runHere(["status", "--porcelain"]);
|
|
58345
|
+
const staged = [];
|
|
58346
|
+
const unstaged = [];
|
|
58347
|
+
const untracked = [];
|
|
58348
|
+
for (const line of status_short.split(`
|
|
58349
|
+
`).filter(Boolean)) {
|
|
58350
|
+
const code = line.slice(0, 2);
|
|
58351
|
+
const file = line.slice(3);
|
|
58352
|
+
if (code === "??") {
|
|
58353
|
+
untracked.push(file);
|
|
58354
|
+
} else {
|
|
58355
|
+
if (code[0] !== " " && code[0] !== "?")
|
|
58356
|
+
staged.push(file);
|
|
58357
|
+
if (code[1] !== " " && code[1] !== "?")
|
|
58358
|
+
unstaged.push(file);
|
|
58325
58359
|
}
|
|
58326
|
-
return null;
|
|
58327
58360
|
}
|
|
58328
|
-
const
|
|
58329
|
-
|
|
58361
|
+
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
58362
|
+
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
58363
|
+
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
58364
|
+
const stagedSet = new Set(staged);
|
|
58365
|
+
const unstagedSet = new Set(unstaged);
|
|
58366
|
+
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
58367
|
+
return {
|
|
58368
|
+
workspace_root: cwd,
|
|
58369
|
+
branch,
|
|
58370
|
+
status_short,
|
|
58371
|
+
staged,
|
|
58372
|
+
unstaged,
|
|
58373
|
+
untracked,
|
|
58374
|
+
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
58375
|
+
`),
|
|
58376
|
+
partial_staged: partial_staged.length > 0,
|
|
58377
|
+
partial_staged_files: partial_staged,
|
|
58378
|
+
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
58379
|
+
};
|
|
58380
|
+
};
|
|
58381
|
+
|
|
58382
|
+
// packages/workit-core/src/core/vcs-config.ts
|
|
58383
|
+
import fs8 from "node:fs";
|
|
58384
|
+
import path20 from "node:path";
|
|
58385
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
58386
|
+
var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE";
|
|
58387
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path20.join(configDir(), "vcs.json");
|
|
58388
|
+
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
58389
|
+
var remoteProvider = (cwd) => {
|
|
58390
|
+
const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
58391
|
+
if (r.status !== 0)
|
|
58330
58392
|
return null;
|
|
58331
|
-
|
|
58393
|
+
const url = (r.stdout ?? "").trim();
|
|
58394
|
+
if (/github\.com[:/]/.test(url))
|
|
58395
|
+
return "github";
|
|
58396
|
+
if (/gitlab\.com[:/]/.test(url))
|
|
58397
|
+
return "gitlab";
|
|
58398
|
+
return null;
|
|
58332
58399
|
};
|
|
58333
|
-
var
|
|
58334
|
-
|
|
58335
|
-
let
|
|
58400
|
+
var readVcsConfig = () => {
|
|
58401
|
+
const file = vcsConfigPath();
|
|
58402
|
+
let raw;
|
|
58336
58403
|
try {
|
|
58337
|
-
|
|
58404
|
+
raw = fs8.readFileSync(file, "utf8");
|
|
58338
58405
|
} catch {
|
|
58339
|
-
return
|
|
58406
|
+
return { status: "missing", path: file, config: {} };
|
|
58340
58407
|
}
|
|
58341
|
-
|
|
58342
|
-
return [];
|
|
58343
|
-
const steps = value.steps;
|
|
58344
|
-
if (!Array.isArray(steps))
|
|
58345
|
-
return null;
|
|
58346
|
-
const normalized = normalizeChainSteps(steps);
|
|
58347
|
-
return normalized;
|
|
58348
|
-
};
|
|
58349
|
-
var decisionMatches = (entry, purpose, binding) => {
|
|
58350
|
-
const decision = entry.data;
|
|
58351
|
-
return binding.taskId === decision.binding.taskId && binding.workspaceId === decision.binding.workspaceId && decision.purpose === purpose && decision.response === "approved" && decision.revoked === null && (purpose !== "action" || decision.consumption === null) && decision.digest === decisionDigest(decision) && scopeEqual(decision.binding.scope, binding.scope) && canonicalJson(decision.binding) === canonicalJson(binding);
|
|
58352
|
-
};
|
|
58353
|
-
var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
58354
|
-
var validProvenance = (value, caller) => {
|
|
58355
|
-
if (!provenanceSchema.safeParse(value).success)
|
|
58356
|
-
return false;
|
|
58357
|
-
const provenance = value;
|
|
58358
|
-
return provenance.kind === "host_observed" && provenance.host === caller.host && provenance.session?.kind === "host" && provenance.session.host === caller.host && provenance.session.handle === caller.actor && provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host);
|
|
58359
|
-
};
|
|
58360
|
-
var verifyNativeDecision = (verifier, input, binding) => {
|
|
58361
|
-
if (!verifier)
|
|
58362
|
-
return failure("permission_denied", "native authority verifier is unavailable");
|
|
58363
|
-
const result = verifier.verifyDecision(input);
|
|
58364
|
-
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
58365
|
-
return failure("permission_denied", "native decision observation was not attested");
|
|
58366
|
-
return success(null, null, trustedAuthority("decision", result.data, {
|
|
58367
|
-
taskId: input.expected.taskId,
|
|
58368
|
-
workspaceId: input.expected.workspaceId,
|
|
58369
|
-
purpose: input.expected.purpose,
|
|
58370
|
-
response: input.expected.response,
|
|
58371
|
-
binding: input.expected.binding,
|
|
58372
|
-
bindingBytes: input.expected.bindingBytes,
|
|
58373
|
-
digest: input.expected.digest,
|
|
58374
|
-
requirementIds: [...input.expected.requirementIds]
|
|
58375
|
-
}, binding, input.caller));
|
|
58376
|
-
};
|
|
58377
|
-
var verifyNativeAction = (verifier, input, binding) => {
|
|
58378
|
-
if (!verifier)
|
|
58379
|
-
return failure("permission_denied", "native authority verifier is unavailable");
|
|
58380
|
-
const result = verifier.verifyAction(input);
|
|
58381
|
-
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
58382
|
-
return failure("permission_denied", "native action observation was not attested");
|
|
58383
|
-
return success(null, null, trustedAuthority("action", result.data, {
|
|
58384
|
-
taskId: input.expected.taskId,
|
|
58385
|
-
workspaceId: input.expected.workspaceId,
|
|
58386
|
-
decisionId: input.expected.decisionId,
|
|
58387
|
-
actionRef: input.expected.actionRef,
|
|
58388
|
-
taskRevision: input.expected.taskRevision,
|
|
58389
|
-
workspaceRevision: input.expected.workspaceRevision,
|
|
58390
|
-
outcome: input.expected.outcome,
|
|
58391
|
-
purpose: input.expected.decision.purpose,
|
|
58392
|
-
response: input.expected.decision.response,
|
|
58393
|
-
binding: input.expected.decision.binding,
|
|
58394
|
-
digest: input.expected.decision.digest,
|
|
58395
|
-
requirementIds: [...input.expected.decision.requirementIds]
|
|
58396
|
-
}, binding, input.caller));
|
|
58397
|
-
};
|
|
58398
|
-
var verifyNativeReconciliation = (verifier, input, binding) => {
|
|
58399
|
-
if (!verifier?.verifyReconciliation)
|
|
58400
|
-
return failure("permission_denied", "native reconciliation authority is unavailable");
|
|
58401
|
-
const result = verifier.verifyReconciliation(input);
|
|
58402
|
-
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
58403
|
-
return failure("permission_denied", "native reconciliation observation was not attested");
|
|
58404
|
-
return success(null, null, trustedAuthority("reconciliation", result.data, {
|
|
58405
|
-
taskId: input.expected.taskId,
|
|
58406
|
-
workspaceId: input.expected.workspaceId,
|
|
58407
|
-
decisionId: input.expected.decisionId,
|
|
58408
|
-
actionRef: input.expected.actionRef,
|
|
58409
|
-
taskRevision: input.expected.taskRevision,
|
|
58410
|
-
workspaceRevision: input.expected.workspaceRevision,
|
|
58411
|
-
outcome: input.expected.outcome,
|
|
58412
|
-
evidenceDigest: input.expected.evidenceDigest,
|
|
58413
|
-
step: input.expected.step,
|
|
58414
|
-
purpose: input.expected.decision.purpose,
|
|
58415
|
-
response: input.expected.decision.response,
|
|
58416
|
-
binding: input.expected.decision.binding,
|
|
58417
|
-
digest: input.expected.decision.digest,
|
|
58418
|
-
requirementIds: [...input.expected.decision.requirementIds]
|
|
58419
|
-
}, binding, input.caller));
|
|
58420
|
-
};
|
|
58421
|
-
var takeAuthority = (authority, binding, caller) => {
|
|
58422
|
-
const record = verifiedAuthorities.get(authority);
|
|
58423
|
-
verifiedAuthorities.delete(authority);
|
|
58424
|
-
if (!record)
|
|
58425
|
-
return null;
|
|
58426
|
-
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
58427
|
-
return null;
|
|
58428
|
-
return record;
|
|
58429
|
-
};
|
|
58430
|
-
var retireNativeAuthority = (authority) => {
|
|
58431
|
-
const record = verifiedAuthorities.get(authority);
|
|
58432
|
-
verifiedAuthorities.delete(authority);
|
|
58433
|
-
return record?.provenance ?? null;
|
|
58434
|
-
};
|
|
58435
|
-
var authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome;
|
|
58436
|
-
var authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds);
|
|
58437
|
-
var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
|
|
58438
|
-
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
58439
|
-
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
58440
|
-
return [];
|
|
58441
|
-
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
58442
|
-
}
|
|
58443
|
-
var digestBytes3 = (bytes) => createHash6("sha256").update(bytes).digest("hex");
|
|
58444
|
-
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
58445
|
-
let root;
|
|
58408
|
+
let parsed;
|
|
58446
58409
|
try {
|
|
58447
|
-
|
|
58410
|
+
parsed = JSON.parse(raw);
|
|
58448
58411
|
} catch {
|
|
58449
|
-
return
|
|
58412
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
58450
58413
|
}
|
|
58451
|
-
|
|
58452
|
-
|
|
58453
|
-
continue;
|
|
58454
|
-
if (!reference.digest)
|
|
58455
|
-
return failure("invalid_input", "document references require a byte digest");
|
|
58456
|
-
const target = path20.resolve(root, reference.path);
|
|
58457
|
-
if (target !== root && !target.startsWith(`${root}${path20.sep}`))
|
|
58458
|
-
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
58459
|
-
fields: [
|
|
58460
|
-
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
58461
|
-
]
|
|
58462
|
-
});
|
|
58463
|
-
try {
|
|
58464
|
-
const relative = path20.relative(root, target);
|
|
58465
|
-
let current = root;
|
|
58466
|
-
for (const segment of relative.split(path20.sep)) {
|
|
58467
|
-
if (!segment)
|
|
58468
|
-
continue;
|
|
58469
|
-
current = path20.join(current, segment);
|
|
58470
|
-
const stat = fs8.lstatSync(current);
|
|
58471
|
-
const real = fs8.realpathSync(current);
|
|
58472
|
-
if (real !== root && !real.startsWith(`${root}${path20.sep}`))
|
|
58473
|
-
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
58474
|
-
fields: [
|
|
58475
|
-
{
|
|
58476
|
-
path: "contentRefs.path",
|
|
58477
|
-
reason: `${reference.path} resolves outside the checkout`
|
|
58478
|
-
}
|
|
58479
|
-
]
|
|
58480
|
-
});
|
|
58481
|
-
if (stat.isSymbolicLink())
|
|
58482
|
-
return failure("invalid_input", "document reference uses a symlink");
|
|
58483
|
-
if (current === target && !stat.isFile())
|
|
58484
|
-
return failure("invalid_input", "document reference is not a regular file");
|
|
58485
|
-
if (current !== target && !stat.isDirectory())
|
|
58486
|
-
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
58487
|
-
}
|
|
58488
|
-
if (digestBytes3(fs8.readFileSync(target)) !== reference.digest)
|
|
58489
|
-
return failure("permission_denied", "approved document bytes have changed");
|
|
58490
|
-
} catch {
|
|
58491
|
-
return failure("invalid_input", "approved document is unavailable");
|
|
58492
|
-
}
|
|
58414
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
58415
|
+
return { status: "valid", path: file, config: parsed };
|
|
58493
58416
|
}
|
|
58494
|
-
return
|
|
58495
|
-
};
|
|
58496
|
-
var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
|
|
58497
|
-
var storedDecisionApplicable = (store, task, purpose, binding) => task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && verifyDecisionContentAtRoot(store.root, entry.data.binding).ok);
|
|
58498
|
-
var validateAction = (store, task, workspaceId, input, authority) => {
|
|
58499
|
-
if (task.status !== "active")
|
|
58500
|
-
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
58501
|
-
if (!refSchema.safeParse(input.actionRef).success)
|
|
58502
|
-
return failure("invalid_input", "action reference is invalid");
|
|
58503
|
-
const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
58504
|
-
if (!entry)
|
|
58505
|
-
return failure("not_found", "decision not found");
|
|
58506
|
-
const decision = entry.data;
|
|
58507
|
-
if (decision.purpose !== "action")
|
|
58508
|
-
return failure("permission_denied", "decision is not an action approval");
|
|
58509
|
-
if (!authorityMatches(authority, {
|
|
58510
|
-
taskId: task.id,
|
|
58511
|
-
workspaceId,
|
|
58512
|
-
decisionId: input.decisionId,
|
|
58513
|
-
actionRef: input.actionRef,
|
|
58514
|
-
taskRevision: input.expectedRevision,
|
|
58515
|
-
workspaceRevision: input.expectedWorkspaceRevision,
|
|
58516
|
-
outcome: "reserve"
|
|
58517
|
-
}))
|
|
58518
|
-
return failure("permission_denied", "native reservation authority is not bound");
|
|
58519
|
-
if (!authorityDecisionMatches(authority, decision))
|
|
58520
|
-
return failure("permission_denied", "native reservation authority does not match decision");
|
|
58521
|
-
if (decision.response !== "approved")
|
|
58522
|
-
return failure("permission_denied", "decision was rejected");
|
|
58523
|
-
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
58524
|
-
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
58525
|
-
if (decision.revoked)
|
|
58526
|
-
return failure("permission_denied", "decision is revoked");
|
|
58527
|
-
if (decision.digest !== decisionDigest(decision))
|
|
58528
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
58529
|
-
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
58530
|
-
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
58531
|
-
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
58532
|
-
return failure("permission_denied", "action binding does not match the approved decision");
|
|
58533
|
-
const content = verifyContentRefs(store, decision.binding);
|
|
58534
|
-
if (!content.ok)
|
|
58535
|
-
return content;
|
|
58536
|
-
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
58537
|
-
if (requestedSteps === null)
|
|
58538
|
-
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
58539
|
-
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
58540
|
-
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
58541
|
-
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
58542
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
58543
|
-
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
58544
|
-
return failure("permission_denied", "bounded action scope changed");
|
|
58545
|
-
return success(null, null, { entry, workflow });
|
|
58417
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
58546
58418
|
};
|
|
58547
|
-
function
|
|
58548
|
-
|
|
58549
|
-
|
|
58550
|
-
const
|
|
58551
|
-
|
|
58552
|
-
|
|
58553
|
-
|
|
58554
|
-
|
|
58555
|
-
|
|
58556
|
-
|
|
58557
|
-
const
|
|
58558
|
-
|
|
58559
|
-
|
|
58560
|
-
|
|
58561
|
-
|
|
58562
|
-
|
|
58563
|
-
|
|
58564
|
-
|
|
58565
|
-
|
|
58566
|
-
|
|
58567
|
-
|
|
58568
|
-
|
|
58569
|
-
|
|
58570
|
-
|
|
58571
|
-
|
|
58572
|
-
|
|
58573
|
-
|
|
58574
|
-
|
|
58575
|
-
|
|
58576
|
-
|
|
58577
|
-
|
|
58578
|
-
|
|
58579
|
-
|
|
58580
|
-
|
|
58581
|
-
return failure("permission_denied", "action approval is already reserved");
|
|
58582
|
-
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
58583
|
-
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
58584
|
-
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
58585
|
-
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
58586
|
-
return failure("permission_denied", "action step was already completed");
|
|
58587
|
-
const nextDecision = {
|
|
58588
|
-
...entry.data,
|
|
58589
|
-
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
58419
|
+
function vcsConfig(mode, cwd) {
|
|
58420
|
+
const ws = resolveWorkspace(vcsCwd(cwd));
|
|
58421
|
+
const wsVcs = ws?.vcs ?? {};
|
|
58422
|
+
const wsYt = ws?.youtrack ?? {};
|
|
58423
|
+
const wsIssues = ws?.issues ?? {};
|
|
58424
|
+
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
58425
|
+
const root = vcsCwd(cwd);
|
|
58426
|
+
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
58427
|
+
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
58428
|
+
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
58429
|
+
const wp = ws?.branchPolicy ?? {};
|
|
58430
|
+
const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
|
|
58431
|
+
const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
58432
|
+
const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
|
|
58433
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
58434
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
58435
|
+
let issuesProvider = null;
|
|
58436
|
+
let linkOnPr = null;
|
|
58437
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
58438
|
+
issuesProvider = "github";
|
|
58439
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
58440
|
+
}
|
|
58441
|
+
if (mode === "resolve") {
|
|
58442
|
+
if (cfgStatus === "malformed")
|
|
58443
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
58444
|
+
return {
|
|
58445
|
+
ok: true,
|
|
58446
|
+
workspace_name: ws?.name ?? null,
|
|
58447
|
+
provider,
|
|
58448
|
+
defaultTargetBranch: defaultTarget,
|
|
58449
|
+
link_issues: linkIssues,
|
|
58450
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
58451
|
+
issues_provider: issuesProvider,
|
|
58452
|
+
link_on_pr: linkOnPr
|
|
58590
58453
|
};
|
|
58591
|
-
|
|
58592
|
-
|
|
58593
|
-
|
|
58594
|
-
|
|
58595
|
-
|
|
58596
|
-
|
|
58597
|
-
|
|
58598
|
-
|
|
58599
|
-
return
|
|
58600
|
-
|
|
58601
|
-
|
|
58602
|
-
|
|
58603
|
-
}
|
|
58604
|
-
}
|
|
58605
|
-
|
|
58606
|
-
|
|
58607
|
-
const
|
|
58608
|
-
|
|
58609
|
-
|
|
58610
|
-
|
|
58611
|
-
|
|
58612
|
-
|
|
58613
|
-
|
|
58614
|
-
|
|
58615
|
-
|
|
58454
|
+
}
|
|
58455
|
+
if (cfgStatus === "malformed") {
|
|
58456
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
58457
|
+
}
|
|
58458
|
+
if (cfgStatus === "missing") {
|
|
58459
|
+
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
58460
|
+
}
|
|
58461
|
+
if (provider === null) {
|
|
58462
|
+
return {
|
|
58463
|
+
ok: false,
|
|
58464
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
58465
|
+
configPath: cfgPath
|
|
58466
|
+
};
|
|
58467
|
+
}
|
|
58468
|
+
const prov = cfg[provider] ?? {};
|
|
58469
|
+
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
58470
|
+
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path20.join(configDir(), `${provider}.token`));
|
|
58471
|
+
const tokenPath = path20.resolve(tokenFile);
|
|
58472
|
+
let tokenOk = false;
|
|
58473
|
+
if (fs8.existsSync(tokenPath)) {
|
|
58474
|
+
const token = fs8.readFileSync(tokenPath, "utf8").trim();
|
|
58475
|
+
const placeholder = !token || token === TOKEN_PLACEHOLDER3 || token.startsWith(TOKEN_PLACEHOLDER3);
|
|
58476
|
+
tokenOk = !placeholder;
|
|
58477
|
+
}
|
|
58478
|
+
const out = {
|
|
58479
|
+
ok: true,
|
|
58480
|
+
configPath: path20.resolve(cfgPath),
|
|
58481
|
+
provider,
|
|
58482
|
+
defaultTargetBranch: defaultTarget,
|
|
58483
|
+
pr: cfg.pr ?? {},
|
|
58484
|
+
tokenPath,
|
|
58485
|
+
tokenPresent: fs8.existsSync(tokenPath),
|
|
58486
|
+
tokenReady: tokenOk,
|
|
58487
|
+
workspace_name: ws?.name ?? null,
|
|
58488
|
+
link_issues: linkIssues,
|
|
58489
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
58490
|
+
issues_provider: issuesProvider,
|
|
58491
|
+
link_on_pr: linkOnPr
|
|
58616
58492
|
};
|
|
58617
|
-
|
|
58618
|
-
|
|
58619
|
-
|
|
58620
|
-
|
|
58621
|
-
|
|
58622
|
-
|
|
58623
|
-
|
|
58624
|
-
|
|
58625
|
-
|
|
58493
|
+
if (provider === "gitlab") {
|
|
58494
|
+
out.gitlab = {
|
|
58495
|
+
host: prov.host ?? "gitlab.com",
|
|
58496
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
58497
|
+
};
|
|
58498
|
+
} else if (provider === "github") {
|
|
58499
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
58500
|
+
}
|
|
58501
|
+
if (mode === "summary")
|
|
58502
|
+
delete out.tokenPath;
|
|
58503
|
+
return out;
|
|
58626
58504
|
}
|
|
58627
|
-
function
|
|
58628
|
-
|
|
58629
|
-
|
|
58630
|
-
|
|
58631
|
-
|
|
58632
|
-
|
|
58633
|
-
|
|
58634
|
-
|
|
58635
|
-
|
|
58636
|
-
|
|
58637
|
-
|
|
58638
|
-
|
|
58639
|
-
|
|
58640
|
-
|
|
58641
|
-
|
|
58642
|
-
|
|
58643
|
-
|
|
58644
|
-
|
|
58645
|
-
|
|
58646
|
-
|
|
58647
|
-
|
|
58648
|
-
|
|
58649
|
-
|
|
58650
|
-
|
|
58651
|
-
|
|
58652
|
-
|
|
58653
|
-
|
|
58654
|
-
if (
|
|
58655
|
-
|
|
58656
|
-
if (!authorityMatches(authority, {
|
|
58657
|
-
taskId: input.taskId,
|
|
58658
|
-
workspaceId: workspace.data.id,
|
|
58659
|
-
decisionId: input.decisionId,
|
|
58660
|
-
actionRef: input.actionRef,
|
|
58661
|
-
taskRevision: input.taskRevision,
|
|
58662
|
-
workspaceRevision: input.workspaceRevision,
|
|
58663
|
-
outcome: input.outcome
|
|
58664
|
-
}))
|
|
58665
|
-
return failure("permission_denied", "native settlement authority does not match context");
|
|
58666
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
58667
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
58668
|
-
expectedWorkspaceRevision: input.workspaceRevision,
|
|
58669
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
58670
|
-
});
|
|
58671
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
58672
|
-
if (!entry)
|
|
58673
|
-
return failure("not_found", "decision not found");
|
|
58674
|
-
if (!authorityDecisionMatches(authority, entry.data))
|
|
58675
|
-
return failure("permission_denied", "native settlement authority does not match decision");
|
|
58676
|
-
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
58677
|
-
return failure("permission_denied", "native settlement caller does not match approval");
|
|
58678
|
-
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
58679
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
58680
|
-
if (entry.data.consumption?.state === "uncertain")
|
|
58681
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
58682
|
-
operation: "settle_action",
|
|
58683
|
-
outcome: "unknown"
|
|
58684
|
-
});
|
|
58685
|
-
if (entry.data.consumption?.state !== "reserved")
|
|
58686
|
-
return failure("permission_denied", "action is not reserved");
|
|
58687
|
-
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
58688
|
-
return failure("permission_denied", "settlement reference does not match reservation");
|
|
58689
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
58690
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
58691
|
-
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
58692
|
-
return failure("recovery_required", "bounded action progress is missing");
|
|
58693
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
58694
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
58695
|
-
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
58696
|
-
if (input.outcome === "unknown") {
|
|
58697
|
-
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
58698
|
-
operation: "settle_action",
|
|
58699
|
-
outcome: "unknown"
|
|
58700
|
-
});
|
|
58701
|
-
const uncertain = {
|
|
58702
|
-
...entry.data,
|
|
58703
|
-
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
58704
|
-
};
|
|
58705
|
-
return success(mutation.revision, null, {
|
|
58706
|
-
...current,
|
|
58707
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
58708
|
-
});
|
|
58505
|
+
function mergedPrStyle(limit = 6, cwd) {
|
|
58506
|
+
const cfg = vcsConfig("load", cwd);
|
|
58507
|
+
if (!cfg.ok || !cfg.tokenReady)
|
|
58508
|
+
return { ok: false, error: "vcs not configured" };
|
|
58509
|
+
const provider = cfg.provider;
|
|
58510
|
+
const token = fs8.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
58511
|
+
const examples = [];
|
|
58512
|
+
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
58513
|
+
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
58514
|
+
sections: desc.split(`
|
|
58515
|
+
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
58516
|
+
descriptionPreview: desc.slice(0, 600)
|
|
58517
|
+
});
|
|
58518
|
+
if (provider === "gitlab") {
|
|
58519
|
+
const remote = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
58520
|
+
if (remote.status !== 0)
|
|
58521
|
+
return { ok: false, error: "no origin remote" };
|
|
58522
|
+
const url = (remote.stdout ?? "").trim();
|
|
58523
|
+
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
58524
|
+
if (!m)
|
|
58525
|
+
return { ok: false, error: "not a gitlab.com origin" };
|
|
58526
|
+
const project = m[1];
|
|
58527
|
+
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
58528
|
+
const run = (args) => spawnSync3("glab", ["api", ...args], { cwd, encoding: "utf8", env });
|
|
58529
|
+
let r = run([
|
|
58530
|
+
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
|
|
58531
|
+
]);
|
|
58532
|
+
if (r.status !== 0) {
|
|
58533
|
+
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
58709
58534
|
}
|
|
58710
|
-
if (
|
|
58711
|
-
|
|
58712
|
-
|
|
58713
|
-
|
|
58714
|
-
|
|
58535
|
+
if (r.status !== 0)
|
|
58536
|
+
return { ok: false, error: "could not list merge requests" };
|
|
58537
|
+
for (const mr of JSON.parse(r.stdout ?? "[]")) {
|
|
58538
|
+
const desc = String(mr.description ?? "").trim();
|
|
58539
|
+
examples.push({
|
|
58540
|
+
title: mr.title,
|
|
58541
|
+
url: mr.web_url,
|
|
58542
|
+
squash: mr.squash,
|
|
58543
|
+
...descInfo(desc, true)
|
|
58715
58544
|
});
|
|
58716
58545
|
}
|
|
58717
|
-
|
|
58718
|
-
|
|
58719
|
-
|
|
58720
|
-
|
|
58721
|
-
|
|
58722
|
-
|
|
58723
|
-
|
|
58724
|
-
const actionProgress = workflow.steps.length ? [
|
|
58725
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
58726
|
-
{
|
|
58727
|
-
decisionId: input.decisionId,
|
|
58728
|
-
steps: [...workflow.steps],
|
|
58729
|
-
completedSteps: [...workflow.completed]
|
|
58730
|
-
}
|
|
58731
|
-
] : current.actionProgress ?? [];
|
|
58732
|
-
const next = {
|
|
58733
|
-
...current,
|
|
58734
|
-
actionProgress,
|
|
58735
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
|
|
58736
|
-
};
|
|
58737
|
-
if (!complete) {
|
|
58738
|
-
outcomeResult = success(mutation.revision, input.workspaceRevision, {
|
|
58739
|
-
...entry,
|
|
58740
|
-
data: settled
|
|
58741
|
-
});
|
|
58546
|
+
} else if (provider === "github") {
|
|
58547
|
+
const r = spawnSync3("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
58548
|
+
if (r.status !== 0)
|
|
58549
|
+
return { ok: false, error: "could not list pull requests" };
|
|
58550
|
+
for (const pr of JSON.parse(r.stdout ?? "[]")) {
|
|
58551
|
+
const desc = String(pr.body ?? "").trim();
|
|
58552
|
+
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
58742
58553
|
}
|
|
58743
|
-
|
|
58744
|
-
|
|
58745
|
-
|
|
58746
|
-
|
|
58747
|
-
|
|
58748
|
-
|
|
58749
|
-
|
|
58750
|
-
|
|
58751
|
-
|
|
58752
|
-
|
|
58554
|
+
}
|
|
58555
|
+
return {
|
|
58556
|
+
ok: true,
|
|
58557
|
+
provider,
|
|
58558
|
+
count: examples.length,
|
|
58559
|
+
styleHints: [
|
|
58560
|
+
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
58561
|
+
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
58562
|
+
"Do not paste commit log or diff stat into the body"
|
|
58563
|
+
],
|
|
58564
|
+
examples
|
|
58565
|
+
};
|
|
58753
58566
|
}
|
|
58754
|
-
|
|
58755
|
-
|
|
58756
|
-
|
|
58757
|
-
|
|
58758
|
-
|
|
58759
|
-
|
|
58760
|
-
|
|
58761
|
-
|
|
58762
|
-
|
|
58763
|
-
|
|
58764
|
-
|
|
58765
|
-
|
|
58766
|
-
|
|
58767
|
-
|
|
58768
|
-
|
|
58769
|
-
|
|
58770
|
-
|
|
58771
|
-
|
|
58772
|
-
|
|
58773
|
-
|
|
58774
|
-
|
|
58775
|
-
|
|
58776
|
-
|
|
58777
|
-
|
|
58778
|
-
|
|
58779
|
-
|
|
58780
|
-
|
|
58781
|
-
|
|
58782
|
-
|
|
58783
|
-
|
|
58784
|
-
|
|
58785
|
-
|
|
58786
|
-
|
|
58787
|
-
|
|
58788
|
-
|
|
58789
|
-
|
|
58790
|
-
|
|
58791
|
-
|
|
58567
|
+
|
|
58568
|
+
// packages/workit-core/src/core/branch.ts
|
|
58569
|
+
var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
58570
|
+
var resolveCommitPolicyFor = (workspaceRoot) => resolveCommitPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
58571
|
+
var policy = (root) => resolveBranchPolicyFor(root);
|
|
58572
|
+
var allowedBranch = (root, name) => policy(root).allowed.some((r) => r.test(name));
|
|
58573
|
+
var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
|
|
58574
|
+
var verifyPushIdentity = (root, provider, account) => {
|
|
58575
|
+
if (!account || typeof account !== "string")
|
|
58576
|
+
return { ok: false, error: "push identity requires a configured area account" };
|
|
58577
|
+
const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
|
|
58578
|
+
const bin = provider === "gitlab" ? "glab" : "gh";
|
|
58579
|
+
let login;
|
|
58580
|
+
try {
|
|
58581
|
+
login = execFileSync3(bin, [...probe], {
|
|
58582
|
+
cwd: root,
|
|
58583
|
+
encoding: "utf8",
|
|
58584
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
58585
|
+
}).trim();
|
|
58586
|
+
} catch {
|
|
58587
|
+
return { ok: false, error: `${bin} identity could not be resolved` };
|
|
58588
|
+
}
|
|
58589
|
+
if (!login || login.toLowerCase() !== account.toLowerCase())
|
|
58590
|
+
return {
|
|
58591
|
+
ok: false,
|
|
58592
|
+
error: `push identity ${login || "(unknown)"} does not match area account ${account}`
|
|
58593
|
+
};
|
|
58594
|
+
return { ok: true };
|
|
58595
|
+
};
|
|
58596
|
+
var baseBranch = (cwd) => {
|
|
58597
|
+
const resolved = vcsConfig("resolve", cwd);
|
|
58598
|
+
if (resolved.ok === false)
|
|
58599
|
+
return { error: String(resolved.error) };
|
|
58600
|
+
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
58601
|
+
};
|
|
58602
|
+
var originBaseReady = (cwd, base) => {
|
|
58603
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
58604
|
+
try {
|
|
58605
|
+
try {
|
|
58606
|
+
run(["fetch", "origin", base, "--prune"]);
|
|
58607
|
+
} catch {
|
|
58608
|
+
run(["fetch", "origin", "--prune"]);
|
|
58609
|
+
}
|
|
58610
|
+
try {
|
|
58611
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
|
|
58612
|
+
cwd,
|
|
58613
|
+
stdio: "pipe"
|
|
58792
58614
|
});
|
|
58615
|
+
} catch {
|
|
58616
|
+
return {
|
|
58617
|
+
ok: false,
|
|
58618
|
+
error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`
|
|
58619
|
+
};
|
|
58793
58620
|
}
|
|
58794
|
-
|
|
58795
|
-
|
|
58796
|
-
|
|
58797
|
-
|
|
58798
|
-
|
|
58799
|
-
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
58621
|
+
return { ok: true };
|
|
58622
|
+
} catch (error) {
|
|
58623
|
+
return {
|
|
58624
|
+
ok: false,
|
|
58625
|
+
error: error instanceof Error ? error.message : "ensure-base-branch failed"
|
|
58800
58626
|
};
|
|
58801
|
-
|
|
58802
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
58803
|
-
{
|
|
58804
|
-
decisionId: input.decisionId,
|
|
58805
|
-
steps: workflow.steps,
|
|
58806
|
-
completedSteps: workflow.completed
|
|
58807
|
-
}
|
|
58808
|
-
] : current.actionProgress ?? [];
|
|
58809
|
-
return success(mutation.revision, null, {
|
|
58810
|
-
...current,
|
|
58811
|
-
actionProgress,
|
|
58812
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
58813
|
-
});
|
|
58814
|
-
}, input.native?.now);
|
|
58815
|
-
if (!changed.ok)
|
|
58816
|
-
return changed;
|
|
58817
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
58818
|
-
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
58819
|
-
}
|
|
58820
|
-
var bindingCovers = scopeCovers;
|
|
58821
|
-
var verifyDecisionContent = verifyContentRefs;
|
|
58822
|
-
|
|
58823
|
-
// packages/workit-core/src/core/task-evaluation.ts
|
|
58824
|
-
var digestBytes4 = (value) => createHash7("sha256").update(value).digest("hex");
|
|
58825
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path21.sep}`);
|
|
58826
|
-
var relative = (root, candidate) => path21.relative(root, candidate).split(path21.sep).join("/") || ".";
|
|
58827
|
-
var canonicalPath = (value) => {
|
|
58828
|
-
if (typeof value !== "string" || !value)
|
|
58829
|
-
return null;
|
|
58830
|
-
if (value.includes("\\") || value.split("/").includes(".."))
|
|
58831
|
-
return null;
|
|
58832
|
-
const normalized = path21.posix.normalize(value);
|
|
58833
|
-
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
58834
|
-
return null;
|
|
58835
|
-
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
58627
|
+
}
|
|
58836
58628
|
};
|
|
58837
|
-
var
|
|
58838
|
-
const
|
|
58839
|
-
|
|
58840
|
-
|
|
58841
|
-
|
|
58842
|
-
|
|
58843
|
-
|
|
58844
|
-
|
|
58845
|
-
|
|
58846
|
-
|
|
58847
|
-
|
|
58629
|
+
var fastForwardBase = (cwd, base) => {
|
|
58630
|
+
const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
58631
|
+
try {
|
|
58632
|
+
let hasLocalBase = true;
|
|
58633
|
+
try {
|
|
58634
|
+
execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
|
|
58635
|
+
cwd,
|
|
58636
|
+
stdio: "pipe"
|
|
58637
|
+
});
|
|
58638
|
+
} catch {
|
|
58639
|
+
hasLocalBase = false;
|
|
58640
|
+
}
|
|
58641
|
+
if (hasLocalBase) {
|
|
58642
|
+
run(["checkout", base]);
|
|
58643
|
+
try {
|
|
58644
|
+
run(["merge", "--ff-only", `origin/${base}`]);
|
|
58645
|
+
} catch {}
|
|
58646
|
+
} else {
|
|
58647
|
+
run(["checkout", "-b", base, "--track", `origin/${base}`]);
|
|
58648
|
+
}
|
|
58649
|
+
return { ok: true };
|
|
58650
|
+
} catch (error) {
|
|
58651
|
+
return {
|
|
58652
|
+
ok: false,
|
|
58653
|
+
error: error instanceof Error ? error.message : "ensure-base-branch failed"
|
|
58654
|
+
};
|
|
58655
|
+
}
|
|
58848
58656
|
};
|
|
58849
|
-
var
|
|
58850
|
-
|
|
58851
|
-
|
|
58852
|
-
|
|
58853
|
-
|
|
58854
|
-
|
|
58855
|
-
|
|
58657
|
+
var ensureBaseBranch = (cwd, base) => {
|
|
58658
|
+
const git = gitContext(cwd);
|
|
58659
|
+
if (!git.branch || git.branch === "unknown")
|
|
58660
|
+
return { ok: false, error: "not in a git repository" };
|
|
58661
|
+
const ready = originBaseReady(cwd, base);
|
|
58662
|
+
if (!ready.ok)
|
|
58663
|
+
return ready;
|
|
58664
|
+
return fastForwardBase(cwd, base);
|
|
58856
58665
|
};
|
|
58857
|
-
var
|
|
58858
|
-
|
|
58859
|
-
|
|
58860
|
-
|
|
58861
|
-
|
|
58862
|
-
|
|
58863
|
-
|
|
58666
|
+
var classifyBranchDirt = (workspaceRoot) => {
|
|
58667
|
+
let raw;
|
|
58668
|
+
try {
|
|
58669
|
+
raw = execFileSync3("git", ["status", "--porcelain=v1"], {
|
|
58670
|
+
cwd: path21.resolve(workspaceRoot),
|
|
58671
|
+
encoding: "utf8",
|
|
58672
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
58673
|
+
});
|
|
58674
|
+
} catch {
|
|
58675
|
+
return "clean";
|
|
58864
58676
|
}
|
|
58865
|
-
|
|
58677
|
+
let dirty = false;
|
|
58678
|
+
for (const line of raw.split(`
|
|
58679
|
+
`)) {
|
|
58680
|
+
if (!line.trim())
|
|
58681
|
+
continue;
|
|
58682
|
+
dirty = true;
|
|
58683
|
+
const code = line.slice(0, 2);
|
|
58684
|
+
if (code === "??")
|
|
58685
|
+
continue;
|
|
58686
|
+
let entry = line.slice(3);
|
|
58687
|
+
const arrow = entry.indexOf(" -> ");
|
|
58688
|
+
if (arrow >= 0)
|
|
58689
|
+
entry = entry.slice(arrow + 4);
|
|
58690
|
+
const unquoted = entry.startsWith('"') && entry.endsWith('"') ? entry.slice(1, -1) : entry;
|
|
58691
|
+
if (unquoted !== "docs" && !unquoted.startsWith("docs/"))
|
|
58692
|
+
return "stash-required";
|
|
58693
|
+
}
|
|
58694
|
+
return dirty ? "carry" : "clean";
|
|
58866
58695
|
};
|
|
58867
|
-
var
|
|
58868
|
-
|
|
58869
|
-
|
|
58870
|
-
|
|
58871
|
-
|
|
58872
|
-
|
|
58873
|
-
|
|
58696
|
+
var branchSetup = ({
|
|
58697
|
+
action,
|
|
58698
|
+
sdd_dir,
|
|
58699
|
+
target_branch,
|
|
58700
|
+
stash,
|
|
58701
|
+
workspace_root,
|
|
58702
|
+
log
|
|
58703
|
+
}) => {
|
|
58704
|
+
const cwd = path21.resolve(workspace_root);
|
|
58705
|
+
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
58706
|
+
const current = gitContext(cwd).branch;
|
|
58707
|
+
if (!current || current === "unknown")
|
|
58708
|
+
return { error: "not in a git repository", phase: "preflight" };
|
|
58709
|
+
const sdd = sdd_dir ?? "docs";
|
|
58710
|
+
const manifestPath = path21.isAbsolute(sdd) ? path21.join(sdd, "manifest.json") : path21.join(cwd, sdd, "manifest.json");
|
|
58711
|
+
const ensureManifestDir = () => {
|
|
58712
|
+
const dir = path21.dirname(manifestPath);
|
|
58713
|
+
if (!existsSync15(dir))
|
|
58714
|
+
mkdirSync8(dir, { recursive: true, mode: 493 });
|
|
58715
|
+
};
|
|
58716
|
+
const readManifest = () => {
|
|
58717
|
+
try {
|
|
58718
|
+
return JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
58719
|
+
} catch {
|
|
58720
|
+
return {};
|
|
58721
|
+
}
|
|
58722
|
+
};
|
|
58723
|
+
const writeManifest = (data) => {
|
|
58724
|
+
ensureManifestDir();
|
|
58725
|
+
writeFileSync10(manifestPath, JSON.stringify(data, null, 2) + `
|
|
58726
|
+
`, "utf8");
|
|
58727
|
+
};
|
|
58728
|
+
const journal = (message) => log?.(`branch-setup: ${message}`);
|
|
58729
|
+
if (action === "reapply_stash") {
|
|
58730
|
+
const manifest = readManifest();
|
|
58731
|
+
const ref = manifest.stash_ref;
|
|
58732
|
+
if (!ref)
|
|
58733
|
+
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
58734
|
+
journal(`pre-pop: ${String(ref)}`);
|
|
58735
|
+
try {
|
|
58736
|
+
exec(["stash", "pop", String(ref)]);
|
|
58737
|
+
} catch (error) {
|
|
58738
|
+
journal("pop: failed");
|
|
58739
|
+
return {
|
|
58740
|
+
error: error instanceof Error ? error.message : "stash pop failed",
|
|
58741
|
+
phase: "post_mutation"
|
|
58742
|
+
};
|
|
58743
|
+
}
|
|
58744
|
+
journal("pop: ok");
|
|
58745
|
+
delete manifest.stash_ref;
|
|
58746
|
+
delete manifest.stash_created_at;
|
|
58747
|
+
writeManifest(manifest);
|
|
58748
|
+
return { action: "reapply_stash", ok: true };
|
|
58749
|
+
}
|
|
58750
|
+
const target = target_branch ?? "";
|
|
58751
|
+
if (!target)
|
|
58874
58752
|
return {
|
|
58875
|
-
|
|
58876
|
-
|
|
58877
|
-
|
|
58753
|
+
error: "target_branch (the working branch to create or switch to) is required for setup",
|
|
58754
|
+
phase: "preflight"
|
|
58755
|
+
};
|
|
58756
|
+
if (isProtected(cwd, target))
|
|
58757
|
+
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
58758
|
+
if (!allowedBranch(cwd, target))
|
|
58759
|
+
return {
|
|
58760
|
+
error: `target branch ${target} is not allowed by the branch policy`,
|
|
58761
|
+
phase: "preflight"
|
|
58878
58762
|
};
|
|
58763
|
+
let base;
|
|
58764
|
+
let targetExists = true;
|
|
58765
|
+
try {
|
|
58766
|
+
exec(["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]);
|
|
58767
|
+
} catch {
|
|
58768
|
+
targetExists = false;
|
|
58769
|
+
}
|
|
58770
|
+
if (!targetExists) {
|
|
58771
|
+
const baseResolved = baseBranch(cwd);
|
|
58772
|
+
if ("error" in baseResolved)
|
|
58773
|
+
return { error: baseResolved.error, phase: "preflight" };
|
|
58774
|
+
base = baseResolved.base;
|
|
58775
|
+
}
|
|
58776
|
+
journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
|
|
58777
|
+
let stash_ref;
|
|
58778
|
+
const failAfterStash = (message, phase = "post_mutation") => {
|
|
58779
|
+
let suffix = "";
|
|
58780
|
+
if (stash_ref) {
|
|
58781
|
+
journal(`pre-pop: ${stash_ref}`);
|
|
58782
|
+
try {
|
|
58783
|
+
exec(["stash", "pop", stash_ref]);
|
|
58784
|
+
stash_ref = undefined;
|
|
58785
|
+
journal("pop: ok");
|
|
58786
|
+
} catch {
|
|
58787
|
+
journal("pop: failed");
|
|
58788
|
+
suffix = " (changes preserved in stash)";
|
|
58789
|
+
}
|
|
58790
|
+
}
|
|
58791
|
+
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
58792
|
+
};
|
|
58793
|
+
if (current !== target) {
|
|
58794
|
+
const dirty = Boolean(gitContext(cwd).status_short.trim());
|
|
58795
|
+
if (classifyBranchDirt(cwd) === "stash-required" && stash !== "yes") {
|
|
58796
|
+
return {
|
|
58797
|
+
error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
|
|
58798
|
+
phase: "preflight"
|
|
58799
|
+
};
|
|
58800
|
+
}
|
|
58801
|
+
let validatedBase;
|
|
58802
|
+
if (!targetExists && base !== undefined) {
|
|
58803
|
+
const ready = originBaseReady(cwd, base);
|
|
58804
|
+
if (!ready.ok)
|
|
58805
|
+
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
58806
|
+
validatedBase = base;
|
|
58807
|
+
}
|
|
58808
|
+
if (dirty && stash === "yes") {
|
|
58809
|
+
try {
|
|
58810
|
+
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
58811
|
+
} catch (error) {
|
|
58812
|
+
return {
|
|
58813
|
+
error: error instanceof Error ? error.message : "stash push failed",
|
|
58814
|
+
phase: "post_mutation"
|
|
58815
|
+
};
|
|
58816
|
+
}
|
|
58817
|
+
stash_ref = "stash@{0}";
|
|
58818
|
+
journal(`stash push: ${stash_ref}`);
|
|
58819
|
+
}
|
|
58820
|
+
try {
|
|
58821
|
+
exec(["checkout", target]);
|
|
58822
|
+
journal("post-checkout");
|
|
58823
|
+
} catch (error) {
|
|
58824
|
+
const message = error instanceof Error ? error.message : "checkout failed";
|
|
58825
|
+
if (/worktree/i.test(message)) {
|
|
58826
|
+
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
|
|
58827
|
+
}
|
|
58828
|
+
try {
|
|
58829
|
+
let effectiveBase = base;
|
|
58830
|
+
if (effectiveBase === undefined) {
|
|
58831
|
+
const lateResolved = baseBranch(cwd);
|
|
58832
|
+
if ("error" in lateResolved)
|
|
58833
|
+
return failAfterStash(lateResolved.error, "preflight");
|
|
58834
|
+
effectiveBase = lateResolved.base;
|
|
58835
|
+
}
|
|
58836
|
+
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
58837
|
+
if (!baseResult.ok)
|
|
58838
|
+
return failAfterStash(baseResult.error ?? "ensure-base-branch failed");
|
|
58839
|
+
exec(["checkout", "-b", target]);
|
|
58840
|
+
journal("post-create");
|
|
58841
|
+
} catch (createError) {
|
|
58842
|
+
return failAfterStash(createError instanceof Error ? createError.message : "branch create failed");
|
|
58843
|
+
}
|
|
58844
|
+
}
|
|
58879
58845
|
}
|
|
58880
|
-
|
|
58881
|
-
|
|
58882
|
-
|
|
58883
|
-
|
|
58884
|
-
if (
|
|
58885
|
-
|
|
58886
|
-
|
|
58887
|
-
|
|
58846
|
+
try {
|
|
58847
|
+
const manifest = readManifest();
|
|
58848
|
+
manifest.branch = target;
|
|
58849
|
+
manifest.previous_branch = current;
|
|
58850
|
+
if (stash_ref) {
|
|
58851
|
+
manifest.stash_ref = stash_ref;
|
|
58852
|
+
manifest.stash_created_at = new Date().toISOString();
|
|
58853
|
+
}
|
|
58854
|
+
writeManifest(manifest);
|
|
58855
|
+
} catch (error) {
|
|
58856
|
+
const result = failAfterStash(error instanceof Error ? `manifest update failed: ${error.message}` : "manifest update failed");
|
|
58857
|
+
if (stash_ref === undefined && gitContext(cwd).branch !== current) {
|
|
58858
|
+
try {
|
|
58859
|
+
exec(["checkout", current]);
|
|
58860
|
+
} catch {
|
|
58861
|
+
try {
|
|
58862
|
+
exec(["checkout", "-m", current]);
|
|
58863
|
+
} catch {}
|
|
58864
|
+
}
|
|
58865
|
+
}
|
|
58866
|
+
return result;
|
|
58888
58867
|
}
|
|
58889
|
-
return {
|
|
58868
|
+
return {
|
|
58869
|
+
action: "setup",
|
|
58870
|
+
ok: true,
|
|
58871
|
+
branch: target,
|
|
58872
|
+
previous_branch: current,
|
|
58873
|
+
stash_ref: stash_ref ?? null,
|
|
58874
|
+
manifest: manifestPath
|
|
58875
|
+
};
|
|
58890
58876
|
};
|
|
58891
|
-
|
|
58892
|
-
|
|
58877
|
+
|
|
58878
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
58879
|
+
var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
|
|
58880
|
+
var OPERATION_CLASS = {
|
|
58881
|
+
"git.branch_setup": "branch",
|
|
58882
|
+
"git.commit": "commit",
|
|
58883
|
+
"git.push": "push",
|
|
58884
|
+
"hosting.pull_request": "pr",
|
|
58885
|
+
"hosting.merge": "merge"
|
|
58886
|
+
};
|
|
58887
|
+
var operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null;
|
|
58888
|
+
var digestOf = (configDirPath) => {
|
|
58893
58889
|
try {
|
|
58894
|
-
|
|
58890
|
+
const raw = readFileSync15(`${configDirPath}/workspaces.json`, "utf8");
|
|
58891
|
+
return createHash6("sha256").update(raw).digest("hex");
|
|
58895
58892
|
} catch {
|
|
58896
|
-
return
|
|
58897
|
-
}
|
|
58898
|
-
let uncertain = false;
|
|
58899
|
-
for (const name of names) {
|
|
58900
|
-
if (name === ".git" || name === ".workit")
|
|
58901
|
-
continue;
|
|
58902
|
-
const target = path21.join(directory, name);
|
|
58903
|
-
let stat;
|
|
58904
|
-
try {
|
|
58905
|
-
stat = fs9.lstatSync(target);
|
|
58906
|
-
} catch {
|
|
58907
|
-
uncertain = true;
|
|
58908
|
-
continue;
|
|
58909
|
-
}
|
|
58910
|
-
const item = relative(root, target);
|
|
58911
|
-
output.add(item);
|
|
58912
|
-
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
58913
|
-
uncertain = walk(root, target, output) || uncertain;
|
|
58893
|
+
return null;
|
|
58914
58894
|
}
|
|
58915
|
-
return uncertain;
|
|
58916
58895
|
};
|
|
58917
|
-
var
|
|
58918
|
-
|
|
58919
|
-
|
|
58920
|
-
|
|
58921
|
-
|
|
58922
|
-
|
|
58923
|
-
|
|
58924
|
-
|
|
58925
|
-
|
|
58926
|
-
try {
|
|
58927
|
-
if (!inside(root, fs9.realpathSync(ancestor)))
|
|
58928
|
-
return failure("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
|
|
58929
|
-
} catch {
|
|
58930
|
-
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
58931
|
-
}
|
|
58932
|
-
roots.push(target);
|
|
58896
|
+
var normalizeClasses = (value) => {
|
|
58897
|
+
if (value === true)
|
|
58898
|
+
return [...AUTO_CLASSES];
|
|
58899
|
+
if (!Array.isArray(value))
|
|
58900
|
+
return [];
|
|
58901
|
+
const seen = new Set;
|
|
58902
|
+
for (const item of value) {
|
|
58903
|
+
if (typeof item === "string" && AUTO_CLASSES.includes(item))
|
|
58904
|
+
seen.add(item);
|
|
58933
58905
|
}
|
|
58934
|
-
return
|
|
58906
|
+
return [...seen];
|
|
58935
58907
|
};
|
|
58936
|
-
|
|
58937
|
-
let
|
|
58908
|
+
var resolveAutoApproval = (root, dir = configDir()) => {
|
|
58909
|
+
let match;
|
|
58938
58910
|
try {
|
|
58939
|
-
|
|
58911
|
+
match = resolveWorkspaceFrom(root, dir);
|
|
58940
58912
|
} catch {
|
|
58941
|
-
return
|
|
58942
|
-
}
|
|
58943
|
-
const normalizedScope = canonicalScope(scope);
|
|
58944
|
-
if (!normalizedScope)
|
|
58945
|
-
return failure("invalid_input", "candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
|
|
58946
|
-
const roots = scopeRoots(checkout, normalizedScope);
|
|
58947
|
-
if (!roots.ok)
|
|
58948
|
-
return roots;
|
|
58949
|
-
const git = gitPaths(checkout);
|
|
58950
|
-
const names = new Set;
|
|
58951
|
-
let uncertain = git.uncertain;
|
|
58952
|
-
for (const target of roots.data) {
|
|
58953
|
-
const item = relative(checkout, target);
|
|
58954
|
-
if (item === ".git" || item === ".workit")
|
|
58955
|
-
continue;
|
|
58956
|
-
names.add(item);
|
|
58957
|
-
if (!fs9.existsSync(target))
|
|
58958
|
-
continue;
|
|
58959
|
-
let stat;
|
|
58960
|
-
try {
|
|
58961
|
-
stat = fs9.lstatSync(target);
|
|
58962
|
-
} catch {
|
|
58963
|
-
uncertain = true;
|
|
58964
|
-
continue;
|
|
58965
|
-
}
|
|
58966
|
-
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
58967
|
-
uncertain = walk(checkout, target, names) || uncertain;
|
|
58968
|
-
}
|
|
58969
|
-
for (const item of git.paths)
|
|
58970
|
-
if (scopeMatches(item, normalizedScope))
|
|
58971
|
-
names.add(item);
|
|
58972
|
-
const files = [];
|
|
58973
|
-
let completeness = uncertain ? "uncertain" : "known";
|
|
58974
|
-
for (const item of [...names].sort()) {
|
|
58975
|
-
if (!scopeMatches(item, normalizedScope))
|
|
58976
|
-
continue;
|
|
58977
|
-
const target = path21.join(checkout, item);
|
|
58978
|
-
try {
|
|
58979
|
-
const stat = fs9.lstatSync(target);
|
|
58980
|
-
if (stat.isSymbolicLink()) {
|
|
58981
|
-
const link = fs9.readlinkSync(target);
|
|
58982
|
-
files.push({ path: item, kind: "symlink", digest: digestBytes4(link), executable: null });
|
|
58983
|
-
} else if (stat.isFile()) {
|
|
58984
|
-
files.push({
|
|
58985
|
-
path: item,
|
|
58986
|
-
kind: "file",
|
|
58987
|
-
digest: digestBytes4(fs9.readFileSync(target)),
|
|
58988
|
-
executable: (stat.mode & 73) !== 0
|
|
58989
|
-
});
|
|
58990
|
-
} else if (!stat.isDirectory()) {
|
|
58991
|
-
completeness = "uncertain";
|
|
58992
|
-
} else if (git.git && git.paths.includes(item)) {
|
|
58993
|
-
completeness = "uncertain";
|
|
58994
|
-
}
|
|
58995
|
-
} catch (error) {
|
|
58996
|
-
if (error?.code === "ENOENT") {
|
|
58997
|
-
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
58998
|
-
} else
|
|
58999
|
-
completeness = "uncertain";
|
|
59000
|
-
}
|
|
58913
|
+
return { status: "off" };
|
|
59001
58914
|
}
|
|
59002
|
-
|
|
59003
|
-
|
|
59004
|
-
|
|
59005
|
-
|
|
59006
|
-
|
|
59007
|
-
|
|
59008
|
-
|
|
59009
|
-
|
|
59010
|
-
|
|
59011
|
-
|
|
59012
|
-
|
|
59013
|
-
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
59014
|
-
completeness = "uncertain";
|
|
59015
|
-
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
59016
|
-
const partial = {
|
|
59017
|
-
id: "0".repeat(64),
|
|
59018
|
-
scope: normalizedScope,
|
|
59019
|
-
completeness,
|
|
59020
|
-
files,
|
|
59021
|
-
environment: values,
|
|
59022
|
-
head
|
|
58915
|
+
if (!match)
|
|
58916
|
+
return { status: "off" };
|
|
58917
|
+
const classes = normalizeClasses(match.autoApprove);
|
|
58918
|
+
if (classes.length === 0)
|
|
58919
|
+
return { status: "off" };
|
|
58920
|
+
return {
|
|
58921
|
+
status: "on",
|
|
58922
|
+
workspace: match.name,
|
|
58923
|
+
classes,
|
|
58924
|
+
account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
|
|
58925
|
+
configDigest: digestOf(dir)
|
|
59023
58926
|
};
|
|
59024
|
-
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
59025
|
-
const parsed = candidateSchema.safeParse(candidate);
|
|
59026
|
-
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
59027
|
-
}
|
|
59028
|
-
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
59029
|
-
var changedPaths = (before, after) => {
|
|
59030
|
-
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
59031
|
-
const left = fileMap(before);
|
|
59032
|
-
const right = fileMap(after);
|
|
59033
|
-
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
59034
|
-
if (before.head !== after.head)
|
|
59035
|
-
changed.push(".");
|
|
59036
|
-
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
59037
|
-
changed.push(".");
|
|
59038
|
-
return changed;
|
|
59039
58927
|
};
|
|
59040
|
-
var
|
|
59041
|
-
if (typeof
|
|
59042
|
-
return
|
|
59043
|
-
|
|
58928
|
+
var autoApproves = (root, cls, dir) => {
|
|
58929
|
+
if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
|
|
58930
|
+
return false;
|
|
58931
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
58932
|
+
return resolved.status === "on" && resolved.classes.includes(cls);
|
|
59044
58933
|
};
|
|
59045
|
-
var
|
|
59046
|
-
|
|
59047
|
-
|
|
58934
|
+
var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
|
|
58935
|
+
var standingApprovalLive = (root, receipt, cls, dir) => {
|
|
58936
|
+
if (!isStandingReceipt(receipt))
|
|
58937
|
+
return false;
|
|
58938
|
+
if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
|
|
58939
|
+
return false;
|
|
58940
|
+
if (receipt.class !== cls)
|
|
58941
|
+
return false;
|
|
58942
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
58943
|
+
if (resolved.status !== "on")
|
|
58944
|
+
return false;
|
|
58945
|
+
if (resolved.workspace !== receipt.workspace)
|
|
58946
|
+
return false;
|
|
58947
|
+
return resolved.classes.includes(cls);
|
|
59048
58948
|
};
|
|
59049
|
-
var
|
|
59050
|
-
|
|
59051
|
-
|
|
59052
|
-
|
|
59053
|
-
|
|
59054
|
-
|
|
59055
|
-
|
|
59056
|
-
|
|
59057
|
-
|
|
59058
|
-
|
|
59059
|
-
|
|
59060
|
-
|
|
59061
|
-
|
|
59062
|
-
|
|
59063
|
-
|
|
59064
|
-
|
|
59065
|
-
|
|
59066
|
-
|
|
59067
|
-
|
|
59068
|
-
|
|
59069
|
-
|
|
59070
|
-
|
|
59071
|
-
|
|
59072
|
-
|
|
59073
|
-
|
|
59074
|
-
|
|
59075
|
-
|
|
59076
|
-
|
|
59077
|
-
|
|
59078
|
-
|
|
59079
|
-
|
|
59080
|
-
const
|
|
59081
|
-
|
|
59082
|
-
|
|
59083
|
-
if (
|
|
59084
|
-
return
|
|
59085
|
-
const
|
|
59086
|
-
|
|
59087
|
-
|
|
59088
|
-
|
|
59089
|
-
|
|
59090
|
-
|
|
59091
|
-
|
|
59092
|
-
|
|
59093
|
-
|
|
59094
|
-
|
|
58949
|
+
var standingReceiptFor = (root, workspace, cls, dir) => {
|
|
58950
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
58951
|
+
return {
|
|
58952
|
+
kind: "standing",
|
|
58953
|
+
workspace,
|
|
58954
|
+
class: cls,
|
|
58955
|
+
configDigest: resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null
|
|
58956
|
+
};
|
|
58957
|
+
};
|
|
58958
|
+
var verifyStandingApproval = (root, task, caller, binding) => {
|
|
58959
|
+
if (task.status !== "active")
|
|
58960
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
58961
|
+
const session = task.intent?.provenance?.session;
|
|
58962
|
+
if (!session || session.kind !== "host" || session.host !== caller.host || session.handle !== caller.actor)
|
|
58963
|
+
return failure("permission_denied", "standing approval requires the lead session");
|
|
58964
|
+
const standing = binding.standing;
|
|
58965
|
+
if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
|
|
58966
|
+
return failure("invalid_input", "standing approval needs a workspace and class");
|
|
58967
|
+
let operation;
|
|
58968
|
+
try {
|
|
58969
|
+
operation = typeof binding.approvedContent === "string" ? JSON.parse(binding.approvedContent).operation : null;
|
|
58970
|
+
} catch {
|
|
58971
|
+
operation = null;
|
|
58972
|
+
}
|
|
58973
|
+
const cls = operationAutoClass(operation);
|
|
58974
|
+
if (!cls || cls !== standing.class)
|
|
58975
|
+
return failure("permission_denied", "standing approval does not cover this operation");
|
|
58976
|
+
const resolved = resolveAutoApproval(root);
|
|
58977
|
+
if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
|
|
58978
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
58979
|
+
if (cls === "push") {
|
|
58980
|
+
const workspace = resolveWorkspaceFrom(root, configDir());
|
|
58981
|
+
const provider = workspace?.vcs?.provider;
|
|
58982
|
+
const account = resolved.account;
|
|
58983
|
+
if (!provider || !account)
|
|
58984
|
+
return failure("permission_denied", "auto-push requires a workspace provider and area account");
|
|
58985
|
+
const identity = verifyPushIdentity(root, provider, account);
|
|
58986
|
+
if (!identity.ok)
|
|
58987
|
+
return failure("permission_denied", identity.error);
|
|
58988
|
+
}
|
|
58989
|
+
return success(null, null, {
|
|
58990
|
+
kind: "host_observed",
|
|
58991
|
+
host: caller.host,
|
|
58992
|
+
session: { kind: "host", host: caller.host, handle: caller.actor },
|
|
58993
|
+
workerId: null,
|
|
58994
|
+
receipts: [standingReceiptFor(root, standing.workspace, cls)]
|
|
59095
58995
|
});
|
|
59096
|
-
}
|
|
59097
|
-
var scopeCovers2 = scopeCovers;
|
|
59098
|
-
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
59099
|
-
const pin = assignment.candidateId ?? null;
|
|
59100
|
-
if (pin === null)
|
|
59101
|
-
return true;
|
|
59102
|
-
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
59103
58996
|
};
|
|
59104
|
-
var
|
|
59105
|
-
|
|
59106
|
-
|
|
58997
|
+
var standingAutoApplies = (store, host, actor, operation) => {
|
|
58998
|
+
let descriptor;
|
|
58999
|
+
try {
|
|
59000
|
+
descriptor = JSON.parse(operation);
|
|
59001
|
+
} catch {
|
|
59002
|
+
return null;
|
|
59003
|
+
}
|
|
59004
|
+
const cls = operationAutoClass(descriptor.operation);
|
|
59005
|
+
if (!cls || !autoApproves(store.root, cls))
|
|
59006
|
+
return null;
|
|
59007
|
+
const listed = store.listTasks();
|
|
59008
|
+
const workspace = store.readWorkspace();
|
|
59009
|
+
if (!listed.ok || !workspace.ok || !workspace.data)
|
|
59010
|
+
return null;
|
|
59011
|
+
const matches = listed.data.filter((task) => task.status === "active" && task.workspaceId === workspace.data.id && task.intent?.provenance?.session?.kind === "host" && task.intent.provenance.session.host === host && task.intent.provenance.session.handle === actor);
|
|
59012
|
+
if (matches.length !== 1)
|
|
59013
|
+
return null;
|
|
59014
|
+
const resolved = resolveAutoApproval(store.root);
|
|
59015
|
+
if (resolved.status !== "on")
|
|
59016
|
+
return null;
|
|
59107
59017
|
return {
|
|
59108
|
-
|
|
59109
|
-
|
|
59018
|
+
task: matches[0],
|
|
59019
|
+
workspaceId: workspace.data.id,
|
|
59020
|
+
workspaceRevision: workspace.data.revision,
|
|
59021
|
+
cls,
|
|
59022
|
+
workspaceName: resolved.workspace
|
|
59110
59023
|
};
|
|
59111
59024
|
};
|
|
59112
|
-
var
|
|
59113
|
-
|
|
59114
|
-
|
|
59115
|
-
|
|
59116
|
-
|
|
59117
|
-
return
|
|
59118
|
-
|
|
59119
|
-
|
|
59120
|
-
if (
|
|
59121
|
-
return
|
|
59122
|
-
|
|
59123
|
-
|
|
59124
|
-
|
|
59125
|
-
|
|
59126
|
-
|
|
59025
|
+
var standingAutoBinding = (core, store, host, actor, operation) => {
|
|
59026
|
+
let descriptor;
|
|
59027
|
+
try {
|
|
59028
|
+
descriptor = JSON.parse(operation);
|
|
59029
|
+
} catch {
|
|
59030
|
+
return null;
|
|
59031
|
+
}
|
|
59032
|
+
const cls = operationAutoClass(descriptor.operation);
|
|
59033
|
+
if (!cls || !autoApproves(store.root, cls))
|
|
59034
|
+
return null;
|
|
59035
|
+
const applies = standingAutoApplies(store, host, actor, operation);
|
|
59036
|
+
if (!applies)
|
|
59037
|
+
return null;
|
|
59038
|
+
const { task } = applies;
|
|
59039
|
+
const resolved = resolveAutoApproval(store.root);
|
|
59040
|
+
if (resolved.status !== "on")
|
|
59041
|
+
return null;
|
|
59042
|
+
const presented = `Workit decision: action — auto-approved ${String(descriptor.operation)} ` + `under workspace rule "${resolved.workspace}" (no question asked).`;
|
|
59043
|
+
const recorded = core.observeStandingDecision({
|
|
59044
|
+
schemaVersion: 1,
|
|
59045
|
+
action: "record",
|
|
59046
|
+
taskId: task.id,
|
|
59047
|
+
expectedRevision: task.revision,
|
|
59048
|
+
purpose: "action",
|
|
59049
|
+
binding: {
|
|
59050
|
+
taskId: task.id,
|
|
59051
|
+
workspaceId: applies.workspaceId,
|
|
59052
|
+
scope: task.intent.data.scope,
|
|
59053
|
+
presented,
|
|
59054
|
+
approvedContent: operation,
|
|
59055
|
+
contentRefs: [],
|
|
59056
|
+
standing: { workspace: resolved.workspace, class: cls }
|
|
59057
|
+
},
|
|
59058
|
+
response: "approved",
|
|
59059
|
+
requirementIds: []
|
|
59060
|
+
});
|
|
59061
|
+
if (!recorded.ok)
|
|
59062
|
+
return null;
|
|
59063
|
+
const fresh = store.readTask(task.id);
|
|
59064
|
+
if (!fresh.ok)
|
|
59065
|
+
return null;
|
|
59066
|
+
return {
|
|
59067
|
+
taskId: task.id,
|
|
59068
|
+
decisionId: recorded.data.id,
|
|
59069
|
+
binding: recorded.data.data.binding,
|
|
59070
|
+
expectedRevision: fresh.data.revision,
|
|
59071
|
+
expectedWorkspaceRevision: applies.workspaceRevision
|
|
59072
|
+
};
|
|
59127
59073
|
};
|
|
59128
|
-
|
|
59129
|
-
|
|
59130
|
-
|
|
59131
|
-
|
|
59132
|
-
|
|
59133
|
-
|
|
59134
|
-
|
|
59135
|
-
if (dimension === "investigation" || dimension === "challenge")
|
|
59136
|
-
return "investigation";
|
|
59137
|
-
if (dimension === "artifacts" || dimension === "continuity")
|
|
59138
|
-
return "artifact";
|
|
59139
|
-
if (dimension === "decisions")
|
|
59140
|
-
return "decision";
|
|
59141
|
-
return "worker report";
|
|
59074
|
+
|
|
59075
|
+
// packages/workit-core/src/core/authority.ts
|
|
59076
|
+
var verifiedAuthorities = new WeakMap;
|
|
59077
|
+
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
59078
|
+
const token = {};
|
|
59079
|
+
verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
|
|
59080
|
+
return token;
|
|
59142
59081
|
};
|
|
59143
|
-
|
|
59144
|
-
|
|
59145
|
-
|
|
59146
|
-
|
|
59147
|
-
|
|
59148
|
-
|
|
59149
|
-
|
|
59150
|
-
|
|
59151
|
-
|
|
59152
|
-
|
|
59153
|
-
|
|
59154
|
-
|
|
59155
|
-
const passed = related.filter(({ entry, evaluation }) => {
|
|
59156
|
-
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
59157
|
-
return false;
|
|
59158
|
-
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
59159
|
-
return false;
|
|
59160
|
-
if (requirement.dimension !== "review")
|
|
59161
|
-
return true;
|
|
59162
|
-
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
59163
|
-
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
59164
|
-
const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
|
|
59165
|
-
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
59166
|
-
});
|
|
59167
|
-
if (passed.length) {
|
|
59168
|
-
const currentId = candidate?.id ?? null;
|
|
59169
|
-
const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
|
|
59170
|
-
if (withRed.length)
|
|
59171
|
-
return {
|
|
59172
|
-
requirementId: requirement.id,
|
|
59173
|
-
status: "satisfied",
|
|
59174
|
-
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
59175
|
-
decisionIds: [],
|
|
59176
|
-
reason: "fresh applicable evidence passed"
|
|
59177
|
-
};
|
|
59178
|
-
if (requirement.dimension === "testing")
|
|
59179
|
-
return {
|
|
59180
|
-
requirementId: requirement.id,
|
|
59181
|
-
status: "unsatisfied",
|
|
59182
|
-
evidenceIds: [],
|
|
59183
|
-
decisionIds: [],
|
|
59184
|
-
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
59185
|
-
};
|
|
59082
|
+
var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
|
|
59083
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59084
|
+
var normalizeChainSteps = (steps) => {
|
|
59085
|
+
if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
|
|
59086
|
+
return null;
|
|
59087
|
+
const normalized = [];
|
|
59088
|
+
for (const step of steps) {
|
|
59089
|
+
if (typeof step === "string") {
|
|
59090
|
+
if (!step.trim())
|
|
59091
|
+
return null;
|
|
59092
|
+
normalized.push({ kind: "commit", message: step });
|
|
59093
|
+
continue;
|
|
59186
59094
|
}
|
|
59187
|
-
if (
|
|
59188
|
-
|
|
59189
|
-
|
|
59190
|
-
|
|
59191
|
-
|
|
59192
|
-
requirementId: requirement.id,
|
|
59193
|
-
status: "satisfied",
|
|
59194
|
-
evidenceIds: [],
|
|
59195
|
-
decisionIds: [],
|
|
59196
|
-
reason: "bounded helper completed and its result is recorded"
|
|
59197
|
-
};
|
|
59095
|
+
if (!isRecord4(step))
|
|
59096
|
+
return null;
|
|
59097
|
+
if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
|
|
59098
|
+
normalized.push({ kind: "branch", target: step.branch });
|
|
59099
|
+
continue;
|
|
59198
59100
|
}
|
|
59199
|
-
|
|
59200
|
-
|
|
59201
|
-
|
|
59202
|
-
|
|
59203
|
-
|
|
59204
|
-
|
|
59205
|
-
|
|
59206
|
-
|
|
59207
|
-
|
|
59208
|
-
|
|
59209
|
-
|
|
59210
|
-
|
|
59211
|
-
|
|
59212
|
-
|
|
59213
|
-
|
|
59214
|
-
|
|
59215
|
-
|
|
59216
|
-
|
|
59217
|
-
|
|
59218
|
-
|
|
59219
|
-
|
|
59220
|
-
|
|
59221
|
-
|
|
59222
|
-
|
|
59223
|
-
|
|
59224
|
-
|
|
59225
|
-
|
|
59226
|
-
|
|
59227
|
-
|
|
59228
|
-
|
|
59229
|
-
|
|
59230
|
-
|
|
59231
|
-
|
|
59232
|
-
|
|
59233
|
-
|
|
59101
|
+
if (step.pr === true && Object.keys(step).length === 1) {
|
|
59102
|
+
normalized.push({ kind: "pr" });
|
|
59103
|
+
continue;
|
|
59104
|
+
}
|
|
59105
|
+
return null;
|
|
59106
|
+
}
|
|
59107
|
+
const keys = normalized.map(chainStepKey);
|
|
59108
|
+
if (new Set(keys).size !== keys.length)
|
|
59109
|
+
return null;
|
|
59110
|
+
return normalized;
|
|
59111
|
+
};
|
|
59112
|
+
var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
59113
|
+
var parseSteps = (content) => {
|
|
59114
|
+
let value;
|
|
59115
|
+
try {
|
|
59116
|
+
value = JSON.parse(content);
|
|
59117
|
+
} catch {
|
|
59118
|
+
return [];
|
|
59119
|
+
}
|
|
59120
|
+
if (typeof value !== "object" || value === null || !("steps" in value))
|
|
59121
|
+
return [];
|
|
59122
|
+
const steps = value.steps;
|
|
59123
|
+
if (!Array.isArray(steps))
|
|
59124
|
+
return null;
|
|
59125
|
+
const normalized = normalizeChainSteps(steps);
|
|
59126
|
+
return normalized;
|
|
59127
|
+
};
|
|
59128
|
+
var decisionMatches = (entry, purpose, binding) => {
|
|
59129
|
+
const decision = entry.data;
|
|
59130
|
+
return binding.taskId === decision.binding.taskId && binding.workspaceId === decision.binding.workspaceId && decision.purpose === purpose && decision.response === "approved" && decision.revoked === null && (purpose !== "action" || decision.consumption === null) && decision.digest === decisionDigest(decision) && scopeEqual(decision.binding.scope, binding.scope) && canonicalJson(decision.binding) === canonicalJson(binding);
|
|
59131
|
+
};
|
|
59132
|
+
var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
59133
|
+
var validProvenance = (value, caller) => {
|
|
59134
|
+
if (!provenanceSchema.safeParse(value).success)
|
|
59135
|
+
return false;
|
|
59136
|
+
const provenance = value;
|
|
59137
|
+
return provenance.kind === "host_observed" && provenance.host === caller.host && provenance.session?.kind === "host" && provenance.session.host === caller.host && provenance.session.handle === caller.actor && provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host);
|
|
59138
|
+
};
|
|
59139
|
+
var verifyNativeDecision = (verifier, input, binding) => {
|
|
59140
|
+
if (!verifier)
|
|
59141
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
59142
|
+
const result = verifier.verifyDecision(input);
|
|
59143
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
59144
|
+
return failure("permission_denied", "native decision observation was not attested");
|
|
59145
|
+
return success(null, null, trustedAuthority("decision", result.data, {
|
|
59146
|
+
taskId: input.expected.taskId,
|
|
59147
|
+
workspaceId: input.expected.workspaceId,
|
|
59148
|
+
purpose: input.expected.purpose,
|
|
59149
|
+
response: input.expected.response,
|
|
59150
|
+
binding: input.expected.binding,
|
|
59151
|
+
bindingBytes: input.expected.bindingBytes,
|
|
59152
|
+
digest: input.expected.digest,
|
|
59153
|
+
requirementIds: [...input.expected.requirementIds]
|
|
59154
|
+
}, binding, input.caller));
|
|
59155
|
+
};
|
|
59156
|
+
var verifyNativeAction = (verifier, input, binding) => {
|
|
59157
|
+
if (!verifier)
|
|
59158
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
59159
|
+
const result = verifier.verifyAction(input);
|
|
59160
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
59161
|
+
return failure("permission_denied", "native action observation was not attested");
|
|
59162
|
+
return success(null, null, trustedAuthority("action", result.data, {
|
|
59163
|
+
taskId: input.expected.taskId,
|
|
59164
|
+
workspaceId: input.expected.workspaceId,
|
|
59165
|
+
decisionId: input.expected.decisionId,
|
|
59166
|
+
actionRef: input.expected.actionRef,
|
|
59167
|
+
taskRevision: input.expected.taskRevision,
|
|
59168
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
59169
|
+
outcome: input.expected.outcome,
|
|
59170
|
+
purpose: input.expected.decision.purpose,
|
|
59171
|
+
response: input.expected.decision.response,
|
|
59172
|
+
binding: input.expected.decision.binding,
|
|
59173
|
+
digest: input.expected.decision.digest,
|
|
59174
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
59175
|
+
}, binding, input.caller));
|
|
59176
|
+
};
|
|
59177
|
+
var verifyNativeReconciliation = (verifier, input, binding) => {
|
|
59178
|
+
if (!verifier?.verifyReconciliation)
|
|
59179
|
+
return failure("permission_denied", "native reconciliation authority is unavailable");
|
|
59180
|
+
const result = verifier.verifyReconciliation(input);
|
|
59181
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
59182
|
+
return failure("permission_denied", "native reconciliation observation was not attested");
|
|
59183
|
+
return success(null, null, trustedAuthority("reconciliation", result.data, {
|
|
59184
|
+
taskId: input.expected.taskId,
|
|
59185
|
+
workspaceId: input.expected.workspaceId,
|
|
59186
|
+
decisionId: input.expected.decisionId,
|
|
59187
|
+
actionRef: input.expected.actionRef,
|
|
59188
|
+
taskRevision: input.expected.taskRevision,
|
|
59189
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
59190
|
+
outcome: input.expected.outcome,
|
|
59191
|
+
evidenceDigest: input.expected.evidenceDigest,
|
|
59192
|
+
step: input.expected.step,
|
|
59193
|
+
purpose: input.expected.decision.purpose,
|
|
59194
|
+
response: input.expected.decision.response,
|
|
59195
|
+
binding: input.expected.decision.binding,
|
|
59196
|
+
digest: input.expected.decision.digest,
|
|
59197
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
59198
|
+
}, binding, input.caller));
|
|
59199
|
+
};
|
|
59200
|
+
var takeAuthority = (authority, binding, caller) => {
|
|
59201
|
+
const record = verifiedAuthorities.get(authority);
|
|
59202
|
+
verifiedAuthorities.delete(authority);
|
|
59203
|
+
if (!record)
|
|
59204
|
+
return null;
|
|
59205
|
+
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
59206
|
+
return null;
|
|
59207
|
+
return record;
|
|
59208
|
+
};
|
|
59209
|
+
var retireNativeAuthority = (authority) => {
|
|
59210
|
+
const record = verifiedAuthorities.get(authority);
|
|
59211
|
+
verifiedAuthorities.delete(authority);
|
|
59212
|
+
return record?.provenance ?? null;
|
|
59213
|
+
};
|
|
59214
|
+
var authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome;
|
|
59215
|
+
var authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds);
|
|
59216
|
+
var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
|
|
59217
|
+
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
59218
|
+
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
59219
|
+
return [];
|
|
59220
|
+
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
59234
59221
|
}
|
|
59235
|
-
|
|
59236
|
-
|
|
59237
|
-
|
|
59238
|
-
|
|
59239
|
-
|
|
59240
|
-
|
|
59241
|
-
|
|
59242
|
-
|
|
59243
|
-
|
|
59244
|
-
|
|
59245
|
-
|
|
59246
|
-
|
|
59247
|
-
|
|
59248
|
-
|
|
59249
|
-
|
|
59250
|
-
|
|
59251
|
-
|
|
59252
|
-
|
|
59253
|
-
|
|
59254
|
-
|
|
59255
|
-
|
|
59222
|
+
var digestBytes3 = (bytes) => createHash7("sha256").update(bytes).digest("hex");
|
|
59223
|
+
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
59224
|
+
let root;
|
|
59225
|
+
try {
|
|
59226
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
59227
|
+
} catch {
|
|
59228
|
+
return failure("permission_denied", "checkout root is unavailable");
|
|
59229
|
+
}
|
|
59230
|
+
for (const reference of binding.contentRefs) {
|
|
59231
|
+
if (reference.kind !== "file")
|
|
59232
|
+
continue;
|
|
59233
|
+
if (!reference.digest)
|
|
59234
|
+
return failure("invalid_input", "document references require a byte digest");
|
|
59235
|
+
const target = path22.resolve(root, reference.path);
|
|
59236
|
+
if (target !== root && !target.startsWith(`${root}${path22.sep}`))
|
|
59237
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
59238
|
+
fields: [
|
|
59239
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
59240
|
+
]
|
|
59241
|
+
});
|
|
59242
|
+
try {
|
|
59243
|
+
const relative = path22.relative(root, target);
|
|
59244
|
+
let current = root;
|
|
59245
|
+
for (const segment of relative.split(path22.sep)) {
|
|
59246
|
+
if (!segment)
|
|
59247
|
+
continue;
|
|
59248
|
+
current = path22.join(current, segment);
|
|
59249
|
+
const stat = fs9.lstatSync(current);
|
|
59250
|
+
const real = fs9.realpathSync(current);
|
|
59251
|
+
if (real !== root && !real.startsWith(`${root}${path22.sep}`))
|
|
59252
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
59253
|
+
fields: [
|
|
59254
|
+
{
|
|
59255
|
+
path: "contentRefs.path",
|
|
59256
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
59257
|
+
}
|
|
59258
|
+
]
|
|
59256
59259
|
});
|
|
59257
|
-
|
|
59258
|
-
|
|
59259
|
-
|
|
59260
|
-
|
|
59261
|
-
|
|
59262
|
-
|
|
59263
|
-
const decisionEntry = view.task.decisions.find((item) => item.id === id);
|
|
59264
|
-
const decision = decisionEntry?.data;
|
|
59265
|
-
return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
|
|
59266
|
-
});
|
|
59267
|
-
if (!valid)
|
|
59268
|
-
return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
59260
|
+
if (stat.isSymbolicLink())
|
|
59261
|
+
return failure("invalid_input", "document reference uses a symlink");
|
|
59262
|
+
if (current === target && !stat.isFile())
|
|
59263
|
+
return failure("invalid_input", "document reference is not a regular file");
|
|
59264
|
+
if (current !== target && !stat.isDirectory())
|
|
59265
|
+
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
59269
59266
|
}
|
|
59267
|
+
if (digestBytes3(fs9.readFileSync(target)) !== reference.digest)
|
|
59268
|
+
return failure("permission_denied", "approved document bytes have changed");
|
|
59269
|
+
} catch {
|
|
59270
|
+
return failure("invalid_input", "approved document is unavailable");
|
|
59270
59271
|
}
|
|
59271
59272
|
}
|
|
59272
|
-
|
|
59273
|
-
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
59274
|
-
return false;
|
|
59275
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
59276
|
-
return !requirement || requirement.before === "close";
|
|
59277
|
-
});
|
|
59278
|
-
if (requestedOutcome !== "stopped" && blocking.length)
|
|
59279
|
-
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
59280
|
-
requirementIds: blocking.map((item) => item.requirementId),
|
|
59281
|
-
requirements: blocking.map((item) => {
|
|
59282
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
59283
|
-
return {
|
|
59284
|
-
requirementId: item.requirementId,
|
|
59285
|
-
ruleId: requirement?.ruleId ?? "unknown",
|
|
59286
|
-
reason: item.reason,
|
|
59287
|
-
satisfaction: requirement?.satisfaction ?? "",
|
|
59288
|
-
dependentAction: requirement?.dependentAction ?? null
|
|
59289
|
-
};
|
|
59290
|
-
})
|
|
59291
|
-
});
|
|
59292
|
-
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
59293
|
-
if (requestedOutcome === "verified" && accepted.length)
|
|
59294
|
-
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
59295
|
-
requirementIds: accepted.map((item) => item.requirementId)
|
|
59296
|
-
});
|
|
59297
|
-
const evidenceIds = [
|
|
59298
|
-
...new Set([
|
|
59299
|
-
...evaluations.flatMap((item) => item.evidenceIds),
|
|
59300
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
59301
|
-
])
|
|
59302
|
-
];
|
|
59303
|
-
const decisionIds = [
|
|
59304
|
-
...new Set([
|
|
59305
|
-
...evaluations.flatMap((item) => item.decisionIds),
|
|
59306
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
59307
|
-
])
|
|
59308
|
-
];
|
|
59309
|
-
return success(null, null, {
|
|
59310
|
-
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
59311
|
-
evidenceIds,
|
|
59312
|
-
decisionIds,
|
|
59313
|
-
requirementIds: evaluations.map((item) => item.requirementId)
|
|
59314
|
-
});
|
|
59315
|
-
}
|
|
59316
|
-
|
|
59317
|
-
// packages/workit-core/src/core/methods.ts
|
|
59318
|
-
var METHODS = {
|
|
59319
|
-
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
59320
|
-
"workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
|
|
59321
|
-
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
59322
|
-
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
59323
|
-
"workit-implement": { dimensions: ["delegation"] },
|
|
59324
|
-
"workit-debug": {},
|
|
59325
|
-
"workit-handoff": {},
|
|
59326
|
-
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
59273
|
+
return success(null, null, null);
|
|
59327
59274
|
};
|
|
59328
|
-
var
|
|
59329
|
-
var
|
|
59330
|
-
|
|
59331
|
-
|
|
59332
|
-
|
|
59333
|
-
|
|
59334
|
-
|
|
59335
|
-
const
|
|
59336
|
-
if (
|
|
59337
|
-
return
|
|
59338
|
-
|
|
59339
|
-
|
|
59340
|
-
|
|
59341
|
-
|
|
59342
|
-
|
|
59343
|
-
|
|
59344
|
-
|
|
59345
|
-
|
|
59275
|
+
var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
|
|
59276
|
+
var storedDecisionApplicable = (store, task, purpose, binding) => task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && verifyDecisionContentAtRoot(store.root, entry.data.binding).ok);
|
|
59277
|
+
var validateAction = (store, task, workspaceId, input, authority) => {
|
|
59278
|
+
if (task.status !== "active")
|
|
59279
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
59280
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
59281
|
+
return failure("invalid_input", "action reference is invalid");
|
|
59282
|
+
const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
59283
|
+
if (!entry)
|
|
59284
|
+
return failure("not_found", "decision not found");
|
|
59285
|
+
const decision = entry.data;
|
|
59286
|
+
if (decision.purpose !== "action")
|
|
59287
|
+
return failure("permission_denied", "decision is not an action approval");
|
|
59288
|
+
if (!authorityMatches(authority, {
|
|
59289
|
+
taskId: task.id,
|
|
59290
|
+
workspaceId,
|
|
59291
|
+
decisionId: input.decisionId,
|
|
59292
|
+
actionRef: input.actionRef,
|
|
59293
|
+
taskRevision: input.expectedRevision,
|
|
59294
|
+
workspaceRevision: input.expectedWorkspaceRevision,
|
|
59295
|
+
outcome: "reserve"
|
|
59296
|
+
}))
|
|
59297
|
+
return failure("permission_denied", "native reservation authority is not bound");
|
|
59298
|
+
if (!authorityDecisionMatches(authority, decision))
|
|
59299
|
+
return failure("permission_denied", "native reservation authority does not match decision");
|
|
59300
|
+
if (decision.response !== "approved")
|
|
59301
|
+
return failure("permission_denied", "decision was rejected");
|
|
59302
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
59303
|
+
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
59304
|
+
const receipts = entry.provenance.receipts;
|
|
59305
|
+
const standing = receipts.filter((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "standing");
|
|
59306
|
+
const questionBacked = receipts.some((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "host");
|
|
59307
|
+
if (!questionBacked && standing.length > 0) {
|
|
59308
|
+
let operation;
|
|
59309
|
+
try {
|
|
59310
|
+
operation = JSON.parse(decision.binding.approvedContent).operation;
|
|
59311
|
+
} catch {
|
|
59312
|
+
operation = null;
|
|
59313
|
+
}
|
|
59314
|
+
const cls = operationAutoClass(operation);
|
|
59315
|
+
if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
|
|
59316
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
59317
|
+
}
|
|
59318
|
+
if (decision.revoked)
|
|
59319
|
+
return failure("permission_denied", "decision is revoked");
|
|
59320
|
+
if (decision.digest !== decisionDigest(decision))
|
|
59321
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
59322
|
+
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
59323
|
+
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
59324
|
+
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
59325
|
+
return failure("permission_denied", "action binding does not match the approved decision");
|
|
59326
|
+
const content = verifyContentRefs(store, decision.binding);
|
|
59327
|
+
if (!content.ok)
|
|
59328
|
+
return content;
|
|
59329
|
+
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
59330
|
+
if (requestedSteps === null)
|
|
59331
|
+
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
59332
|
+
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
59333
|
+
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
59334
|
+
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
59335
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
59336
|
+
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
59337
|
+
return failure("permission_denied", "bounded action scope changed");
|
|
59338
|
+
return success(null, null, { entry, workflow });
|
|
59346
59339
|
};
|
|
59347
|
-
function
|
|
59348
|
-
|
|
59349
|
-
|
|
59350
|
-
|
|
59351
|
-
|
|
59352
|
-
|
|
59353
|
-
|
|
59354
|
-
|
|
59355
|
-
|
|
59356
|
-
|
|
59357
|
-
|
|
59358
|
-
|
|
59359
|
-
|
|
59360
|
-
|
|
59361
|
-
|
|
59362
|
-
|
|
59363
|
-
|
|
59364
|
-
|
|
59365
|
-
|
|
59366
|
-
|
|
59367
|
-
|
|
59368
|
-
|
|
59369
|
-
|
|
59370
|
-
|
|
59371
|
-
|
|
59372
|
-
|
|
59373
|
-
|
|
59374
|
-
|
|
59375
|
-
|
|
59376
|
-
|
|
59340
|
+
function reserveAction(input) {
|
|
59341
|
+
if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
|
|
59342
|
+
return failure("invalid_input", "action reservation preconditions are required");
|
|
59343
|
+
const authority = takeAuthority(input.authority, {
|
|
59344
|
+
owner: input.authorityOwner,
|
|
59345
|
+
store: input.store,
|
|
59346
|
+
root: input.store.root
|
|
59347
|
+
}, input.authorityCaller);
|
|
59348
|
+
if (!authority)
|
|
59349
|
+
return failure("permission_denied", "native reservation authority is not verified");
|
|
59350
|
+
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
59351
|
+
const workspace = input.store.readWorkspace();
|
|
59352
|
+
if (!workspace.ok)
|
|
59353
|
+
return workspace;
|
|
59354
|
+
if (!workspace.data)
|
|
59355
|
+
return failure("not_found", "workspace not found");
|
|
59356
|
+
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
59357
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
59358
|
+
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
59359
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
59360
|
+
});
|
|
59361
|
+
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
59362
|
+
if (!valid.ok)
|
|
59363
|
+
return valid;
|
|
59364
|
+
const { entry, workflow } = valid.data;
|
|
59365
|
+
const existing = entry.data.consumption;
|
|
59366
|
+
if (existing?.state === "uncertain")
|
|
59367
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
59368
|
+
operation: "reserve_action",
|
|
59369
|
+
outcome: "unknown"
|
|
59370
|
+
});
|
|
59371
|
+
if (existing?.state === "consumed")
|
|
59372
|
+
return failure("permission_denied", "action approval is consumed");
|
|
59373
|
+
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
59374
|
+
return failure("permission_denied", "action approval is already reserved");
|
|
59375
|
+
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
59376
|
+
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
59377
|
+
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
59378
|
+
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
59379
|
+
return failure("permission_denied", "action step was already completed");
|
|
59380
|
+
const nextDecision = {
|
|
59381
|
+
...entry.data,
|
|
59382
|
+
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
59383
|
+
};
|
|
59384
|
+
const actionProgress = workflow.steps.length ? [
|
|
59385
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
59386
|
+
{
|
|
59387
|
+
decisionId: input.decisionId,
|
|
59388
|
+
steps: [...workflow.steps],
|
|
59389
|
+
completedSteps: [...workflow.completed]
|
|
59390
|
+
}
|
|
59391
|
+
] : current.actionProgress ?? [];
|
|
59392
|
+
return success(mutation.revision, null, {
|
|
59393
|
+
...current,
|
|
59394
|
+
actionProgress,
|
|
59395
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
59377
59396
|
});
|
|
59378
|
-
|
|
59379
|
-
|
|
59380
|
-
|
|
59381
|
-
|
|
59382
|
-
|
|
59383
|
-
|
|
59397
|
+
}, input.native?.now);
|
|
59398
|
+
if (!changed.ok)
|
|
59399
|
+
return changed;
|
|
59400
|
+
const workspace = input.store.readWorkspace();
|
|
59401
|
+
if (!workspace.ok || !workspace.data)
|
|
59402
|
+
return failure("recovery_required", "workspace disappeared after reservation");
|
|
59403
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
59404
|
+
if (!entry)
|
|
59405
|
+
return failure("recovery_required", "reserved decision disappeared");
|
|
59406
|
+
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
59407
|
+
steps: [],
|
|
59408
|
+
completedSteps: []
|
|
59409
|
+
};
|
|
59410
|
+
return success(changed.data.revision, workspace.data.revision, {
|
|
59411
|
+
taskId: input.taskId,
|
|
59412
|
+
decisionId: input.decisionId,
|
|
59413
|
+
actionRef: input.actionRef,
|
|
59414
|
+
taskRevision: changed.data.revision,
|
|
59415
|
+
workspaceRevision: workspace.data.revision,
|
|
59416
|
+
completedSteps: [...workflow.completedSteps],
|
|
59417
|
+
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
59384
59418
|
});
|
|
59385
59419
|
}
|
|
59386
|
-
|
|
59387
|
-
|
|
59388
|
-
|
|
59389
|
-
|
|
59390
|
-
|
|
59391
|
-
|
|
59392
|
-
|
|
59393
|
-
|
|
59394
|
-
if (
|
|
59395
|
-
return
|
|
59396
|
-
|
|
59397
|
-
|
|
59398
|
-
|
|
59399
|
-
|
|
59400
|
-
|
|
59401
|
-
|
|
59402
|
-
|
|
59403
|
-
|
|
59404
|
-
|
|
59420
|
+
function settleAction(input) {
|
|
59421
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
59422
|
+
return failure("invalid_input", "action settlement preconditions are required");
|
|
59423
|
+
const authority = takeAuthority(input.authority, {
|
|
59424
|
+
owner: input.authorityOwner,
|
|
59425
|
+
store: input.store,
|
|
59426
|
+
root: input.store.root
|
|
59427
|
+
}, input.authorityCaller);
|
|
59428
|
+
if (!authority)
|
|
59429
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
59430
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
59431
|
+
return failure("invalid_input", "action reference is invalid");
|
|
59432
|
+
if (!authorityMatches(authority, {
|
|
59433
|
+
taskId: input.taskId,
|
|
59434
|
+
workspaceId: authority.workspaceId,
|
|
59435
|
+
decisionId: input.decisionId,
|
|
59436
|
+
actionRef: input.actionRef,
|
|
59437
|
+
taskRevision: input.taskRevision,
|
|
59438
|
+
workspaceRevision: input.workspaceRevision,
|
|
59439
|
+
outcome: input.outcome
|
|
59440
|
+
}))
|
|
59441
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
59442
|
+
let outcomeResult = null;
|
|
59443
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
59444
|
+
const workspace = input.store.readWorkspace();
|
|
59445
|
+
if (!workspace.ok)
|
|
59446
|
+
return workspace;
|
|
59447
|
+
if (!workspace.data)
|
|
59448
|
+
return failure("not_found", "workspace not found");
|
|
59449
|
+
if (!authorityMatches(authority, {
|
|
59450
|
+
taskId: input.taskId,
|
|
59451
|
+
workspaceId: workspace.data.id,
|
|
59452
|
+
decisionId: input.decisionId,
|
|
59453
|
+
actionRef: input.actionRef,
|
|
59454
|
+
taskRevision: input.taskRevision,
|
|
59455
|
+
workspaceRevision: input.workspaceRevision,
|
|
59456
|
+
outcome: input.outcome
|
|
59457
|
+
}))
|
|
59458
|
+
return failure("permission_denied", "native settlement authority does not match context");
|
|
59459
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
59460
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
59461
|
+
expectedWorkspaceRevision: input.workspaceRevision,
|
|
59462
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
59463
|
+
});
|
|
59464
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
59465
|
+
if (!entry)
|
|
59466
|
+
return failure("not_found", "decision not found");
|
|
59467
|
+
if (!authorityDecisionMatches(authority, entry.data))
|
|
59468
|
+
return failure("permission_denied", "native settlement authority does not match decision");
|
|
59469
|
+
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
59470
|
+
return failure("permission_denied", "native settlement caller does not match approval");
|
|
59471
|
+
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
59472
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
59473
|
+
if (entry.data.consumption?.state === "uncertain")
|
|
59474
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
59475
|
+
operation: "settle_action",
|
|
59476
|
+
outcome: "unknown"
|
|
59477
|
+
});
|
|
59478
|
+
if (entry.data.consumption?.state !== "reserved")
|
|
59479
|
+
return failure("permission_denied", "action is not reserved");
|
|
59480
|
+
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
59481
|
+
return failure("permission_denied", "settlement reference does not match reservation");
|
|
59482
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
59483
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
59484
|
+
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
59485
|
+
return failure("recovery_required", "bounded action progress is missing");
|
|
59486
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
59487
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
59488
|
+
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
59489
|
+
if (input.outcome === "unknown") {
|
|
59490
|
+
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
59491
|
+
operation: "settle_action",
|
|
59492
|
+
outcome: "unknown"
|
|
59493
|
+
});
|
|
59494
|
+
const uncertain = {
|
|
59495
|
+
...entry.data,
|
|
59496
|
+
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
59497
|
+
};
|
|
59498
|
+
return success(mutation.revision, null, {
|
|
59499
|
+
...current,
|
|
59500
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
59501
|
+
});
|
|
59502
|
+
}
|
|
59503
|
+
if (input.outcome === "not_started") {
|
|
59504
|
+
const released = { ...entry.data, consumption: null };
|
|
59505
|
+
return success(mutation.revision, null, {
|
|
59506
|
+
...current,
|
|
59507
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
|
|
59508
|
+
});
|
|
59509
|
+
}
|
|
59510
|
+
if (currentStep)
|
|
59511
|
+
workflow.completed.push(currentStep);
|
|
59512
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
59513
|
+
const settled = {
|
|
59514
|
+
...entry.data,
|
|
59515
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
59405
59516
|
};
|
|
59406
|
-
|
|
59407
|
-
|
|
59408
|
-
|
|
59409
|
-
|
|
59410
|
-
|
|
59517
|
+
const actionProgress = workflow.steps.length ? [
|
|
59518
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
59519
|
+
{
|
|
59520
|
+
decisionId: input.decisionId,
|
|
59521
|
+
steps: [...workflow.steps],
|
|
59522
|
+
completedSteps: [...workflow.completed]
|
|
59523
|
+
}
|
|
59524
|
+
] : current.actionProgress ?? [];
|
|
59525
|
+
const next = {
|
|
59526
|
+
...current,
|
|
59527
|
+
actionProgress,
|
|
59528
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
|
|
59529
|
+
};
|
|
59530
|
+
if (!complete) {
|
|
59531
|
+
outcomeResult = success(mutation.revision, input.workspaceRevision, {
|
|
59532
|
+
...entry,
|
|
59533
|
+
data: settled
|
|
59534
|
+
});
|
|
59535
|
+
}
|
|
59536
|
+
return success(mutation.revision, null, next);
|
|
59537
|
+
}, input.native?.now);
|
|
59538
|
+
if (!changed.ok)
|
|
59539
|
+
return changed;
|
|
59540
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
59541
|
+
if (!entry)
|
|
59542
|
+
return failure("recovery_required", "settled decision disappeared");
|
|
59543
|
+
if (outcomeResult)
|
|
59544
|
+
return outcomeResult;
|
|
59545
|
+
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
59546
|
+
}
|
|
59547
|
+
function reconcileAction(input) {
|
|
59548
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
59549
|
+
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
59550
|
+
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
59551
|
+
if (!authority || authority.kind !== "reconciliation")
|
|
59552
|
+
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
59553
|
+
if (authority.taskId !== input.taskId || authority.decisionId !== input.decisionId || !sameRef(authority.actionRef, input.actionRef) || authority.taskRevision !== input.taskRevision || authority.workspaceRevision !== input.workspaceRevision || authority.outcome !== input.outcome || authority.evidenceDigest !== input.evidenceDigest || authority.step !== input.step)
|
|
59554
|
+
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
59555
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
59556
|
+
const workspace = input.store.readWorkspace();
|
|
59557
|
+
if (!workspace.ok)
|
|
59558
|
+
return workspace;
|
|
59559
|
+
if (!workspace.data)
|
|
59560
|
+
return failure("not_found", "workspace not found");
|
|
59561
|
+
if (authority.workspaceId !== workspace.data.id)
|
|
59562
|
+
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
59563
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
59564
|
+
return failure("revision_conflict", "workspace revision does not match");
|
|
59565
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
59566
|
+
if (!entry || entry.data.purpose !== "action")
|
|
59567
|
+
return failure("not_found", "decision not found");
|
|
59568
|
+
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
59569
|
+
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
59570
|
+
if (entry.data.response !== "approved" || entry.data.revoked !== null || entry.data.digest !== decisionDigest(entry.data) || entry.data.binding.taskId !== current.id || entry.data.binding.workspaceId !== current.workspaceId || entry.data.binding.workspaceId !== workspace.data.id)
|
|
59571
|
+
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
59572
|
+
if (entry.data.consumption?.state !== "uncertain")
|
|
59573
|
+
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
59574
|
+
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
59575
|
+
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
59576
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
59577
|
+
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
59578
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
59579
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
59580
|
+
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
59581
|
+
if (input.outcome === "not_started") {
|
|
59582
|
+
return success(mutation.revision, null, {
|
|
59583
|
+
...current,
|
|
59584
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
59585
|
+
});
|
|
59586
|
+
}
|
|
59587
|
+
if (currentStep)
|
|
59588
|
+
workflow.completed.push(currentStep);
|
|
59589
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
59590
|
+
const nextDecision = {
|
|
59591
|
+
...entry.data,
|
|
59592
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
59411
59593
|
};
|
|
59412
|
-
|
|
59413
|
-
|
|
59414
|
-
|
|
59415
|
-
|
|
59416
|
-
|
|
59417
|
-
|
|
59418
|
-
|
|
59419
|
-
|
|
59420
|
-
|
|
59421
|
-
|
|
59422
|
-
|
|
59423
|
-
|
|
59424
|
-
|
|
59425
|
-
|
|
59426
|
-
|
|
59427
|
-
|
|
59428
|
-
|
|
59429
|
-
|
|
59430
|
-
...view.task.progress.blockers.map((entry) => entry.reason)
|
|
59431
|
-
])).sort().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
|
|
59432
|
-
const context = {
|
|
59433
|
-
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
59434
|
-
status: view.task.status,
|
|
59435
|
-
decisions,
|
|
59436
|
-
methods,
|
|
59437
|
-
gaps,
|
|
59438
|
-
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
59439
|
-
};
|
|
59440
|
-
let encoded = JSON.stringify(context);
|
|
59441
|
-
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
59442
|
-
if (context.gaps.length)
|
|
59443
|
-
context.gaps.pop();
|
|
59444
|
-
else if (context.decisions.length)
|
|
59445
|
-
context.decisions.pop();
|
|
59446
|
-
else
|
|
59447
|
-
context.methods.pop();
|
|
59448
|
-
encoded = JSON.stringify(context);
|
|
59449
|
-
}
|
|
59450
|
-
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
59451
|
-
context.objective = compactText(context.objective, 64) ?? "";
|
|
59452
|
-
context.nextAction = compactText(context.nextAction, 64);
|
|
59453
|
-
encoded = JSON.stringify(context);
|
|
59454
|
-
}
|
|
59455
|
-
return encoded;
|
|
59594
|
+
const actionProgress = workflow.steps.length ? [
|
|
59595
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
59596
|
+
{
|
|
59597
|
+
decisionId: input.decisionId,
|
|
59598
|
+
steps: workflow.steps,
|
|
59599
|
+
completedSteps: workflow.completed
|
|
59600
|
+
}
|
|
59601
|
+
] : current.actionProgress ?? [];
|
|
59602
|
+
return success(mutation.revision, null, {
|
|
59603
|
+
...current,
|
|
59604
|
+
actionProgress,
|
|
59605
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
59606
|
+
});
|
|
59607
|
+
}, input.native?.now);
|
|
59608
|
+
if (!changed.ok)
|
|
59609
|
+
return changed;
|
|
59610
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
59611
|
+
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
59456
59612
|
}
|
|
59457
|
-
var
|
|
59458
|
-
|
|
59459
|
-
exportedAt: bundle.exportedAt,
|
|
59460
|
-
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
59461
|
-
task: bundle.task
|
|
59462
|
-
});
|
|
59463
|
-
// packages/workit-core/src/core/branch.ts
|
|
59464
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "node:fs";
|
|
59465
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
59466
|
-
import path23 from "node:path";
|
|
59613
|
+
var bindingCovers = scopeCovers;
|
|
59614
|
+
var verifyDecisionContent = verifyContentRefs;
|
|
59467
59615
|
|
|
59468
|
-
// packages/workit-core/src/core/
|
|
59469
|
-
|
|
59470
|
-
var
|
|
59471
|
-
|
|
59472
|
-
|
|
59473
|
-
|
|
59474
|
-
|
|
59475
|
-
|
|
59476
|
-
|
|
59477
|
-
|
|
59478
|
-
|
|
59479
|
-
|
|
59480
|
-
|
|
59481
|
-
stdout: (e.stdout ?? "").trimEnd(),
|
|
59482
|
-
stderr: (e.stderr ?? "").trimEnd(),
|
|
59483
|
-
exitCode: e.status ?? 1
|
|
59484
|
-
};
|
|
59485
|
-
}
|
|
59616
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
59617
|
+
var digestBytes4 = (value) => createHash8("sha256").update(value).digest("hex");
|
|
59618
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path23.sep}`);
|
|
59619
|
+
var relative = (root, candidate) => path23.relative(root, candidate).split(path23.sep).join("/") || ".";
|
|
59620
|
+
var canonicalPath = (value) => {
|
|
59621
|
+
if (typeof value !== "string" || !value)
|
|
59622
|
+
return null;
|
|
59623
|
+
if (value.includes("\\") || value.split("/").includes(".."))
|
|
59624
|
+
return null;
|
|
59625
|
+
const normalized = path23.posix.normalize(value);
|
|
59626
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
59627
|
+
return null;
|
|
59628
|
+
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
59486
59629
|
};
|
|
59487
|
-
var
|
|
59488
|
-
const
|
|
59489
|
-
|
|
59490
|
-
const
|
|
59491
|
-
|
|
59492
|
-
|
|
59493
|
-
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
59494
|
-
}
|
|
59495
|
-
return result.stdout;
|
|
59496
|
-
};
|
|
59497
|
-
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
59498
|
-
const status_short = runHere(["status", "--porcelain"]);
|
|
59499
|
-
const staged = [];
|
|
59500
|
-
const unstaged = [];
|
|
59501
|
-
const untracked = [];
|
|
59502
|
-
for (const line of status_short.split(`
|
|
59503
|
-
`).filter(Boolean)) {
|
|
59504
|
-
const code = line.slice(0, 2);
|
|
59505
|
-
const file = line.slice(3);
|
|
59506
|
-
if (code === "??") {
|
|
59507
|
-
untracked.push(file);
|
|
59508
|
-
} else {
|
|
59509
|
-
if (code[0] !== " " && code[0] !== "?")
|
|
59510
|
-
staged.push(file);
|
|
59511
|
-
if (code[1] !== " " && code[1] !== "?")
|
|
59512
|
-
unstaged.push(file);
|
|
59513
|
-
}
|
|
59514
|
-
}
|
|
59515
|
-
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
59516
|
-
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
59517
|
-
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
59518
|
-
const stagedSet = new Set(staged);
|
|
59519
|
-
const unstagedSet = new Set(unstaged);
|
|
59520
|
-
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
59630
|
+
var canonicalScope = (scope) => {
|
|
59631
|
+
const paths = scope.paths.length ? scope.paths : ["."];
|
|
59632
|
+
const normalizedPaths = paths.map(canonicalPath);
|
|
59633
|
+
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
59634
|
+
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
59635
|
+
return null;
|
|
59521
59636
|
return {
|
|
59522
|
-
|
|
59523
|
-
|
|
59524
|
-
|
|
59525
|
-
staged,
|
|
59526
|
-
unstaged,
|
|
59527
|
-
untracked,
|
|
59528
|
-
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
59529
|
-
`),
|
|
59530
|
-
partial_staged: partial_staged.length > 0,
|
|
59531
|
-
partial_staged_files: partial_staged,
|
|
59532
|
-
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
59637
|
+
description: scope.description,
|
|
59638
|
+
paths: [...new Set(normalizedPaths)],
|
|
59639
|
+
exclusions: [...new Set(normalizedExclusions)]
|
|
59533
59640
|
};
|
|
59534
59641
|
};
|
|
59535
|
-
|
|
59536
|
-
|
|
59537
|
-
|
|
59538
|
-
|
|
59539
|
-
|
|
59540
|
-
|
|
59541
|
-
|
|
59542
|
-
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
59543
|
-
var remoteProvider = (cwd) => {
|
|
59544
|
-
const r = spawnSync4("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
59545
|
-
if (r.status !== 0)
|
|
59546
|
-
return null;
|
|
59547
|
-
const url = (r.stdout ?? "").trim();
|
|
59548
|
-
if (/github\.com[:/]/.test(url))
|
|
59549
|
-
return "github";
|
|
59550
|
-
if (/gitlab\.com[:/]/.test(url))
|
|
59551
|
-
return "gitlab";
|
|
59552
|
-
return null;
|
|
59642
|
+
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
59643
|
+
var scopeMatches = (value, scope) => {
|
|
59644
|
+
const normalized = canonicalScope(scope);
|
|
59645
|
+
if (!normalized)
|
|
59646
|
+
return false;
|
|
59647
|
+
const paths = normalized.paths;
|
|
59648
|
+
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
59553
59649
|
};
|
|
59554
|
-
var
|
|
59555
|
-
|
|
59556
|
-
|
|
59557
|
-
|
|
59558
|
-
|
|
59559
|
-
|
|
59560
|
-
|
|
59650
|
+
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
59651
|
+
var compareCodeUnits2 = (left, right) => {
|
|
59652
|
+
const length = Math.min(left.length, right.length);
|
|
59653
|
+
for (let index = 0;index < length; index += 1) {
|
|
59654
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
59655
|
+
if (difference !== 0)
|
|
59656
|
+
return difference;
|
|
59657
|
+
}
|
|
59658
|
+
return left.length - right.length;
|
|
59659
|
+
};
|
|
59660
|
+
var gitPaths = (root) => {
|
|
59661
|
+
const result = spawnSync4("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
59662
|
+
cwd: root,
|
|
59663
|
+
encoding: "buffer"
|
|
59664
|
+
});
|
|
59665
|
+
if (result.status !== 0) {
|
|
59666
|
+
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
59667
|
+
return {
|
|
59668
|
+
paths: [],
|
|
59669
|
+
uncertain: !/not a git repository/i.test(stderr),
|
|
59670
|
+
git: /not a git repository/i.test(stderr) ? false : true
|
|
59671
|
+
};
|
|
59561
59672
|
}
|
|
59562
|
-
|
|
59673
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path23.sep).join("/")) : [];
|
|
59674
|
+
const stagedDeleted = spawnSync4("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
59675
|
+
if (stagedDeleted.status !== 0) {
|
|
59676
|
+
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
59677
|
+
if (!/not a git repository/i.test(stderr))
|
|
59678
|
+
return { paths, uncertain: true, git: true };
|
|
59679
|
+
} else if (stagedDeleted.stdout) {
|
|
59680
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path23.sep).join("/")));
|
|
59681
|
+
}
|
|
59682
|
+
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
59683
|
+
};
|
|
59684
|
+
var walk = (root, directory, output) => {
|
|
59685
|
+
let names;
|
|
59563
59686
|
try {
|
|
59564
|
-
|
|
59687
|
+
names = fs10.readdirSync(directory);
|
|
59565
59688
|
} catch {
|
|
59566
|
-
return
|
|
59689
|
+
return true;
|
|
59567
59690
|
}
|
|
59568
|
-
|
|
59569
|
-
|
|
59691
|
+
let uncertain = false;
|
|
59692
|
+
for (const name of names) {
|
|
59693
|
+
if (name === ".git" || name === ".workit")
|
|
59694
|
+
continue;
|
|
59695
|
+
const target = path23.join(directory, name);
|
|
59696
|
+
let stat;
|
|
59697
|
+
try {
|
|
59698
|
+
stat = fs10.lstatSync(target);
|
|
59699
|
+
} catch {
|
|
59700
|
+
uncertain = true;
|
|
59701
|
+
continue;
|
|
59702
|
+
}
|
|
59703
|
+
const item = relative(root, target);
|
|
59704
|
+
output.add(item);
|
|
59705
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
59706
|
+
uncertain = walk(root, target, output) || uncertain;
|
|
59570
59707
|
}
|
|
59571
|
-
return
|
|
59708
|
+
return uncertain;
|
|
59572
59709
|
};
|
|
59573
|
-
|
|
59574
|
-
const
|
|
59575
|
-
const
|
|
59576
|
-
|
|
59577
|
-
|
|
59578
|
-
|
|
59579
|
-
|
|
59580
|
-
|
|
59581
|
-
|
|
59582
|
-
|
|
59583
|
-
|
|
59584
|
-
|
|
59585
|
-
|
|
59586
|
-
|
|
59587
|
-
|
|
59588
|
-
|
|
59589
|
-
let issuesProvider = null;
|
|
59590
|
-
let linkOnPr = null;
|
|
59591
|
-
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
59592
|
-
issuesProvider = "github";
|
|
59593
|
-
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
59594
|
-
}
|
|
59595
|
-
if (mode === "resolve") {
|
|
59596
|
-
if (cfgStatus === "malformed")
|
|
59597
|
-
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
59598
|
-
return {
|
|
59599
|
-
ok: true,
|
|
59600
|
-
workspace_name: ws?.name ?? null,
|
|
59601
|
-
provider,
|
|
59602
|
-
defaultTargetBranch: defaultTarget,
|
|
59603
|
-
link_issues: linkIssues,
|
|
59604
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
59605
|
-
issues_provider: issuesProvider,
|
|
59606
|
-
link_on_pr: linkOnPr
|
|
59607
|
-
};
|
|
59608
|
-
}
|
|
59609
|
-
if (cfgStatus === "malformed") {
|
|
59610
|
-
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
59710
|
+
var scopeRoots = (root, scope) => {
|
|
59711
|
+
const roots = [];
|
|
59712
|
+
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
59713
|
+
const target = path23.resolve(root, item);
|
|
59714
|
+
if (!inside(root, target))
|
|
59715
|
+
return failure("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
|
|
59716
|
+
let ancestor = target;
|
|
59717
|
+
while (!fs10.existsSync(ancestor) && ancestor !== root)
|
|
59718
|
+
ancestor = path23.dirname(ancestor);
|
|
59719
|
+
try {
|
|
59720
|
+
if (!inside(root, fs10.realpathSync(ancestor)))
|
|
59721
|
+
return failure("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
|
|
59722
|
+
} catch {
|
|
59723
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
59724
|
+
}
|
|
59725
|
+
roots.push(target);
|
|
59611
59726
|
}
|
|
59612
|
-
|
|
59613
|
-
|
|
59727
|
+
return success(null, null, roots);
|
|
59728
|
+
};
|
|
59729
|
+
function captureCandidate(root, scope, environment = []) {
|
|
59730
|
+
let checkout;
|
|
59731
|
+
try {
|
|
59732
|
+
checkout = fs10.realpathSync(root);
|
|
59733
|
+
} catch {
|
|
59734
|
+
return failure("invalid_input", "candidate root does not exist");
|
|
59614
59735
|
}
|
|
59615
|
-
|
|
59616
|
-
|
|
59617
|
-
|
|
59618
|
-
|
|
59619
|
-
|
|
59620
|
-
|
|
59736
|
+
const normalizedScope = canonicalScope(scope);
|
|
59737
|
+
if (!normalizedScope)
|
|
59738
|
+
return failure("invalid_input", "candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
|
|
59739
|
+
const roots = scopeRoots(checkout, normalizedScope);
|
|
59740
|
+
if (!roots.ok)
|
|
59741
|
+
return roots;
|
|
59742
|
+
const git = gitPaths(checkout);
|
|
59743
|
+
const names = new Set;
|
|
59744
|
+
let uncertain = git.uncertain;
|
|
59745
|
+
for (const target of roots.data) {
|
|
59746
|
+
const item = relative(checkout, target);
|
|
59747
|
+
if (item === ".git" || item === ".workit")
|
|
59748
|
+
continue;
|
|
59749
|
+
names.add(item);
|
|
59750
|
+
if (!fs10.existsSync(target))
|
|
59751
|
+
continue;
|
|
59752
|
+
let stat;
|
|
59753
|
+
try {
|
|
59754
|
+
stat = fs10.lstatSync(target);
|
|
59755
|
+
} catch {
|
|
59756
|
+
uncertain = true;
|
|
59757
|
+
continue;
|
|
59758
|
+
}
|
|
59759
|
+
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
59760
|
+
uncertain = walk(checkout, target, names) || uncertain;
|
|
59621
59761
|
}
|
|
59622
|
-
const
|
|
59623
|
-
|
|
59624
|
-
|
|
59625
|
-
const
|
|
59626
|
-
let
|
|
59627
|
-
|
|
59628
|
-
|
|
59629
|
-
|
|
59630
|
-
|
|
59762
|
+
for (const item of git.paths)
|
|
59763
|
+
if (scopeMatches(item, normalizedScope))
|
|
59764
|
+
names.add(item);
|
|
59765
|
+
const files = [];
|
|
59766
|
+
let completeness = uncertain ? "uncertain" : "known";
|
|
59767
|
+
for (const item of [...names].sort()) {
|
|
59768
|
+
if (!scopeMatches(item, normalizedScope))
|
|
59769
|
+
continue;
|
|
59770
|
+
const target = path23.join(checkout, item);
|
|
59771
|
+
try {
|
|
59772
|
+
const stat = fs10.lstatSync(target);
|
|
59773
|
+
if (stat.isSymbolicLink()) {
|
|
59774
|
+
const link = fs10.readlinkSync(target);
|
|
59775
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes4(link), executable: null });
|
|
59776
|
+
} else if (stat.isFile()) {
|
|
59777
|
+
files.push({
|
|
59778
|
+
path: item,
|
|
59779
|
+
kind: "file",
|
|
59780
|
+
digest: digestBytes4(fs10.readFileSync(target)),
|
|
59781
|
+
executable: (stat.mode & 73) !== 0
|
|
59782
|
+
});
|
|
59783
|
+
} else if (!stat.isDirectory()) {
|
|
59784
|
+
completeness = "uncertain";
|
|
59785
|
+
} else if (git.git && git.paths.includes(item)) {
|
|
59786
|
+
completeness = "uncertain";
|
|
59787
|
+
}
|
|
59788
|
+
} catch (error) {
|
|
59789
|
+
if (error?.code === "ENOENT") {
|
|
59790
|
+
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
59791
|
+
} else
|
|
59792
|
+
completeness = "uncertain";
|
|
59793
|
+
}
|
|
59631
59794
|
}
|
|
59632
|
-
const
|
|
59633
|
-
|
|
59634
|
-
|
|
59635
|
-
|
|
59636
|
-
|
|
59637
|
-
|
|
59638
|
-
|
|
59639
|
-
|
|
59640
|
-
|
|
59641
|
-
|
|
59642
|
-
|
|
59643
|
-
|
|
59644
|
-
|
|
59645
|
-
|
|
59795
|
+
const supplied = Array.isArray(environment) ? environment.map((value) => typeof value === "string" ? { name: value, value: process.env[value] ?? null } : { name: value.name, value: value.value ?? null }) : Object.keys(environment).map((name) => ({
|
|
59796
|
+
name,
|
|
59797
|
+
value: environment[name] ?? null
|
|
59798
|
+
}));
|
|
59799
|
+
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
59800
|
+
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
59801
|
+
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
59802
|
+
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
59803
|
+
if (values.some(({ value }) => value === null))
|
|
59804
|
+
completeness = "uncertain";
|
|
59805
|
+
const headResult = spawnSync4("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
59806
|
+
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
59807
|
+
completeness = "uncertain";
|
|
59808
|
+
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
59809
|
+
const partial = {
|
|
59810
|
+
id: "0".repeat(64),
|
|
59811
|
+
scope: normalizedScope,
|
|
59812
|
+
completeness,
|
|
59813
|
+
files,
|
|
59814
|
+
environment: values,
|
|
59815
|
+
head
|
|
59646
59816
|
};
|
|
59647
|
-
|
|
59648
|
-
|
|
59649
|
-
|
|
59650
|
-
|
|
59817
|
+
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
59818
|
+
const parsed = candidateSchema.safeParse(candidate);
|
|
59819
|
+
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
59820
|
+
}
|
|
59821
|
+
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
59822
|
+
var changedPaths = (before, after) => {
|
|
59823
|
+
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
59824
|
+
const left = fileMap(before);
|
|
59825
|
+
const right = fileMap(after);
|
|
59826
|
+
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
59827
|
+
if (before.head !== after.head)
|
|
59828
|
+
changed.push(".");
|
|
59829
|
+
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
59830
|
+
changed.push(".");
|
|
59831
|
+
return changed;
|
|
59832
|
+
};
|
|
59833
|
+
var sessionFromRef = (value) => {
|
|
59834
|
+
if (typeof value !== "object" || value === null || value.kind !== "host")
|
|
59835
|
+
return null;
|
|
59836
|
+
return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
59837
|
+
};
|
|
59838
|
+
var sameSession = (left, right) => {
|
|
59839
|
+
const other = sessionFromRef(right);
|
|
59840
|
+
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
59841
|
+
};
|
|
59842
|
+
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
59843
|
+
function evaluateEvidence(task, candidate) {
|
|
59844
|
+
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
59845
|
+
return task.evidence.map((entry) => {
|
|
59846
|
+
const evidence = entry.data;
|
|
59847
|
+
if (evidence.result === "failed")
|
|
59848
|
+
return {
|
|
59849
|
+
evidenceId: entry.id,
|
|
59850
|
+
status: "failed",
|
|
59851
|
+
reason: "failed evidence remains historical"
|
|
59852
|
+
};
|
|
59853
|
+
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
59854
|
+
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
59855
|
+
return {
|
|
59856
|
+
evidenceId: entry.id,
|
|
59857
|
+
status: "stale",
|
|
59858
|
+
reason: "check or review evidence lacks start and end candidates"
|
|
59859
|
+
};
|
|
59860
|
+
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
59861
|
+
return {
|
|
59862
|
+
evidenceId: entry.id,
|
|
59863
|
+
status: "stale",
|
|
59864
|
+
reason: "candidate changed during the check or review"
|
|
59865
|
+
};
|
|
59866
|
+
}
|
|
59867
|
+
if (!evidence.candidateId || !current)
|
|
59868
|
+
return {
|
|
59869
|
+
evidenceId: entry.id,
|
|
59870
|
+
status: evidence.result,
|
|
59871
|
+
reason: "evidence has no candidate binding"
|
|
59872
|
+
};
|
|
59873
|
+
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
59874
|
+
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
59875
|
+
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
59876
|
+
if (observed.id === current.id)
|
|
59877
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
59878
|
+
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
59879
|
+
const changed = changedPaths(observed, current);
|
|
59880
|
+
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
59881
|
+
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
59882
|
+
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
59883
|
+
return {
|
|
59884
|
+
evidenceId: entry.id,
|
|
59885
|
+
status: evidence.result,
|
|
59886
|
+
reason: "candidate changed outside evidence scope"
|
|
59651
59887
|
};
|
|
59652
|
-
} else if (provider === "github") {
|
|
59653
|
-
out.github = { host: prov.host ?? "github.com" };
|
|
59654
|
-
}
|
|
59655
|
-
if (mode === "summary")
|
|
59656
|
-
delete out.tokenPath;
|
|
59657
|
-
return out;
|
|
59658
|
-
}
|
|
59659
|
-
function mergedPrStyle(limit = 6, cwd) {
|
|
59660
|
-
const cfg = vcsConfig("load", cwd);
|
|
59661
|
-
if (!cfg.ok || !cfg.tokenReady)
|
|
59662
|
-
return { ok: false, error: "vcs not configured" };
|
|
59663
|
-
const provider = cfg.provider;
|
|
59664
|
-
const token = fs10.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
59665
|
-
const examples = [];
|
|
59666
|
-
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
59667
|
-
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
59668
|
-
sections: desc.split(`
|
|
59669
|
-
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
59670
|
-
descriptionPreview: desc.slice(0, 600)
|
|
59671
59888
|
});
|
|
59672
|
-
|
|
59673
|
-
|
|
59674
|
-
|
|
59675
|
-
|
|
59676
|
-
|
|
59677
|
-
|
|
59678
|
-
|
|
59679
|
-
|
|
59680
|
-
|
|
59681
|
-
|
|
59682
|
-
|
|
59683
|
-
let r = run([
|
|
59684
|
-
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
|
|
59685
|
-
]);
|
|
59686
|
-
if (r.status !== 0) {
|
|
59687
|
-
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
59688
|
-
}
|
|
59689
|
-
if (r.status !== 0)
|
|
59690
|
-
return { ok: false, error: "could not list merge requests" };
|
|
59691
|
-
for (const mr of JSON.parse(r.stdout ?? "[]")) {
|
|
59692
|
-
const desc = String(mr.description ?? "").trim();
|
|
59693
|
-
examples.push({
|
|
59694
|
-
title: mr.title,
|
|
59695
|
-
url: mr.web_url,
|
|
59696
|
-
squash: mr.squash,
|
|
59697
|
-
...descInfo(desc, true)
|
|
59698
|
-
});
|
|
59699
|
-
}
|
|
59700
|
-
} else if (provider === "github") {
|
|
59701
|
-
const r = spawnSync4("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
59702
|
-
if (r.status !== 0)
|
|
59703
|
-
return { ok: false, error: "could not list pull requests" };
|
|
59704
|
-
for (const pr of JSON.parse(r.stdout ?? "[]")) {
|
|
59705
|
-
const desc = String(pr.body ?? "").trim();
|
|
59706
|
-
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
59707
|
-
}
|
|
59708
|
-
}
|
|
59889
|
+
}
|
|
59890
|
+
var scopeCovers2 = scopeCovers;
|
|
59891
|
+
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
59892
|
+
const pin = assignment.candidateId ?? null;
|
|
59893
|
+
if (pin === null)
|
|
59894
|
+
return true;
|
|
59895
|
+
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
59896
|
+
};
|
|
59897
|
+
var resolveBinding = (evidence, pin, currentCandidateId) => {
|
|
59898
|
+
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
59899
|
+
const fallback = bound ? pin ?? currentCandidateId : null;
|
|
59709
59900
|
return {
|
|
59710
|
-
|
|
59711
|
-
|
|
59712
|
-
count: examples.length,
|
|
59713
|
-
styleHints: [
|
|
59714
|
-
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
59715
|
-
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
59716
|
-
"Do not paste commit log or diff stat into the body"
|
|
59717
|
-
],
|
|
59718
|
-
examples
|
|
59901
|
+
beforeCandidateId: evidence.beforeCandidateId ?? fallback,
|
|
59902
|
+
candidateId: evidence.candidateId ?? fallback
|
|
59719
59903
|
};
|
|
59720
|
-
}
|
|
59721
|
-
|
|
59722
|
-
// packages/workit-core/src/core/branch.ts
|
|
59723
|
-
var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
59724
|
-
var resolveCommitPolicyFor = (workspaceRoot) => resolveCommitPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
59725
|
-
var policy = (root) => resolveBranchPolicyFor(root);
|
|
59726
|
-
var allowedBranch = (root, name) => policy(root).allowed.some((r) => r.test(name));
|
|
59727
|
-
var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
|
|
59728
|
-
var baseBranch = (cwd) => {
|
|
59729
|
-
const resolved = vcsConfig("resolve", cwd);
|
|
59730
|
-
if (resolved.ok === false)
|
|
59731
|
-
return { error: String(resolved.error) };
|
|
59732
|
-
return { base: String(resolved.defaultTargetBranch ?? "develop") };
|
|
59733
59904
|
};
|
|
59734
|
-
var
|
|
59735
|
-
|
|
59736
|
-
|
|
59737
|
-
|
|
59738
|
-
|
|
59739
|
-
|
|
59740
|
-
|
|
59905
|
+
var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
|
|
59906
|
+
var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
|
|
59907
|
+
var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
|
|
59908
|
+
var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
|
|
59909
|
+
if (kind !== "passed")
|
|
59910
|
+
return false;
|
|
59911
|
+
if (dimension === "testing" || dimension === "verification")
|
|
59912
|
+
return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
|
|
59913
|
+
if (dimension === "review")
|
|
59914
|
+
return evidenceKind === "review";
|
|
59915
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
59916
|
+
return evidenceKind === "investigation";
|
|
59917
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
59918
|
+
return evidenceKind === "artifact";
|
|
59919
|
+
return false;
|
|
59920
|
+
};
|
|
59921
|
+
var expectedKinds = (dimension, ruleId) => {
|
|
59922
|
+
if (dimension === "testing")
|
|
59923
|
+
return "check";
|
|
59924
|
+
if (dimension === "verification")
|
|
59925
|
+
return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
|
|
59926
|
+
if (dimension === "review")
|
|
59927
|
+
return "review";
|
|
59928
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
59929
|
+
return "investigation";
|
|
59930
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
59931
|
+
return "artifact";
|
|
59932
|
+
if (dimension === "decisions")
|
|
59933
|
+
return "decision";
|
|
59934
|
+
return "worker report";
|
|
59935
|
+
};
|
|
59936
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
|
|
59937
|
+
const evidence = evaluateEvidence(task, candidate);
|
|
59938
|
+
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
59939
|
+
return task.policy.requirements.map((requirement) => ({
|
|
59940
|
+
requirementId: requirement.id,
|
|
59941
|
+
status: "unsatisfied",
|
|
59942
|
+
evidenceIds: [],
|
|
59943
|
+
decisionIds: [],
|
|
59944
|
+
reason: "stored policy version is unsupported; reassessment is required"
|
|
59945
|
+
}));
|
|
59946
|
+
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
59947
|
+
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
59948
|
+
const passed = related.filter(({ entry, evaluation }) => {
|
|
59949
|
+
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
59950
|
+
return false;
|
|
59951
|
+
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
59952
|
+
return false;
|
|
59953
|
+
if (requirement.dimension !== "review")
|
|
59954
|
+
return true;
|
|
59955
|
+
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
59956
|
+
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
59957
|
+
const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
|
|
59958
|
+
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
59959
|
+
});
|
|
59960
|
+
if (passed.length) {
|
|
59961
|
+
const currentId = candidate?.id ?? null;
|
|
59962
|
+
const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
|
|
59963
|
+
if (withRed.length)
|
|
59964
|
+
return {
|
|
59965
|
+
requirementId: requirement.id,
|
|
59966
|
+
status: "satisfied",
|
|
59967
|
+
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
59968
|
+
decisionIds: [],
|
|
59969
|
+
reason: "fresh applicable evidence passed"
|
|
59970
|
+
};
|
|
59971
|
+
if (requirement.dimension === "testing")
|
|
59972
|
+
return {
|
|
59973
|
+
requirementId: requirement.id,
|
|
59974
|
+
status: "unsatisfied",
|
|
59975
|
+
evidenceIds: [],
|
|
59976
|
+
decisionIds: [],
|
|
59977
|
+
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
59978
|
+
};
|
|
59741
59979
|
}
|
|
59742
|
-
|
|
59743
|
-
|
|
59744
|
-
|
|
59745
|
-
|
|
59746
|
-
|
|
59747
|
-
|
|
59980
|
+
if (requirement.dimension === "delegation") {
|
|
59981
|
+
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
59982
|
+
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
59983
|
+
if (reconciled)
|
|
59984
|
+
return {
|
|
59985
|
+
requirementId: requirement.id,
|
|
59986
|
+
status: "satisfied",
|
|
59987
|
+
evidenceIds: [],
|
|
59988
|
+
decisionIds: [],
|
|
59989
|
+
reason: "bounded helper completed and its result is recorded"
|
|
59990
|
+
};
|
|
59991
|
+
}
|
|
59992
|
+
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
59993
|
+
if (decisions.length)
|
|
59748
59994
|
return {
|
|
59749
|
-
|
|
59750
|
-
|
|
59995
|
+
requirementId: requirement.id,
|
|
59996
|
+
status: "satisfied",
|
|
59997
|
+
evidenceIds: [],
|
|
59998
|
+
decisionIds: decisions.map((decision) => decision.id),
|
|
59999
|
+
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
60000
|
+
};
|
|
60001
|
+
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
60002
|
+
if (limitations.length)
|
|
60003
|
+
return {
|
|
60004
|
+
requirementId: requirement.id,
|
|
60005
|
+
status: "accepted_limitation",
|
|
60006
|
+
evidenceIds: [],
|
|
60007
|
+
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
60008
|
+
reason: "an applicable approved limitation permits the missing evidence"
|
|
60009
|
+
};
|
|
60010
|
+
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
60011
|
+
if (!unavailable && related.length > 0 && !passed.length)
|
|
60012
|
+
return {
|
|
60013
|
+
requirementId: requirement.id,
|
|
60014
|
+
status: "unsatisfied",
|
|
60015
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
60016
|
+
decisionIds: [],
|
|
60017
|
+
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
59751
60018
|
};
|
|
60019
|
+
return {
|
|
60020
|
+
requirementId: requirement.id,
|
|
60021
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
60022
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
60023
|
+
decisionIds: [],
|
|
60024
|
+
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
60025
|
+
};
|
|
60026
|
+
});
|
|
60027
|
+
}
|
|
60028
|
+
function evaluateClosure(requestedOutcome, view) {
|
|
60029
|
+
if (requestedOutcome !== "stopped" && !view.task.policy)
|
|
60030
|
+
return failure("requirements_unsatisfied", "task has not been assessed");
|
|
60031
|
+
if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
|
|
60032
|
+
return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
|
|
60033
|
+
const evaluations = view.requirements;
|
|
60034
|
+
const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
|
|
60035
|
+
if (requestedOutcome !== "stopped" && openFindings.length)
|
|
60036
|
+
return failure("requirements_unsatisfied", "open findings must be resolved before closure");
|
|
60037
|
+
if (requestedOutcome !== "stopped") {
|
|
60038
|
+
const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
|
|
60039
|
+
for (const entry of view.task.findings) {
|
|
60040
|
+
if (entry.data.disposition === "fixed") {
|
|
60041
|
+
const verified = entry.data.resolution?.evidenceIds.some((id) => {
|
|
60042
|
+
const evidence = view.task.evidence.find((item) => item.id === id);
|
|
60043
|
+
if (!evidence)
|
|
60044
|
+
return false;
|
|
60045
|
+
return findingVerificationPasses(entry.data.candidateId, {
|
|
60046
|
+
kind: evidence.data.kind,
|
|
60047
|
+
candidateId: evidence.data.candidateId,
|
|
60048
|
+
status: evidenceById.get(id) ?? "missing"
|
|
60049
|
+
});
|
|
60050
|
+
});
|
|
60051
|
+
if (!verified)
|
|
60052
|
+
return failure("requirements_unsatisfied", "fixed findings require current verification");
|
|
60053
|
+
}
|
|
60054
|
+
if (entry.data.disposition === "deferred") {
|
|
60055
|
+
const valid = entry.data.resolution?.decisionIds.some((id) => {
|
|
60056
|
+
const decisionEntry = view.task.decisions.find((item) => item.id === id);
|
|
60057
|
+
const decision = decisionEntry?.data;
|
|
60058
|
+
return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
|
|
60059
|
+
});
|
|
60060
|
+
if (!valid)
|
|
60061
|
+
return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
60062
|
+
}
|
|
59752
60063
|
}
|
|
59753
|
-
return { ok: true };
|
|
59754
|
-
} catch (error) {
|
|
59755
|
-
return {
|
|
59756
|
-
ok: false,
|
|
59757
|
-
error: error instanceof Error ? error.message : "ensure-base-branch failed"
|
|
59758
|
-
};
|
|
59759
60064
|
}
|
|
60065
|
+
const blocking = evaluations.filter((item) => {
|
|
60066
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
60067
|
+
return false;
|
|
60068
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
60069
|
+
return !requirement || requirement.before === "close";
|
|
60070
|
+
});
|
|
60071
|
+
if (requestedOutcome !== "stopped" && blocking.length)
|
|
60072
|
+
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
60073
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
60074
|
+
requirements: blocking.map((item) => {
|
|
60075
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
60076
|
+
return {
|
|
60077
|
+
requirementId: item.requirementId,
|
|
60078
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
60079
|
+
reason: item.reason,
|
|
60080
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
60081
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
60082
|
+
};
|
|
60083
|
+
})
|
|
60084
|
+
});
|
|
60085
|
+
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
60086
|
+
if (requestedOutcome === "verified" && accepted.length)
|
|
60087
|
+
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
60088
|
+
requirementIds: accepted.map((item) => item.requirementId)
|
|
60089
|
+
});
|
|
60090
|
+
const evidenceIds = [
|
|
60091
|
+
...new Set([
|
|
60092
|
+
...evaluations.flatMap((item) => item.evidenceIds),
|
|
60093
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
60094
|
+
])
|
|
60095
|
+
];
|
|
60096
|
+
const decisionIds = [
|
|
60097
|
+
...new Set([
|
|
60098
|
+
...evaluations.flatMap((item) => item.decisionIds),
|
|
60099
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
60100
|
+
])
|
|
60101
|
+
];
|
|
60102
|
+
return success(null, null, {
|
|
60103
|
+
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
60104
|
+
evidenceIds,
|
|
60105
|
+
decisionIds,
|
|
60106
|
+
requirementIds: evaluations.map((item) => item.requirementId)
|
|
60107
|
+
});
|
|
60108
|
+
}
|
|
60109
|
+
|
|
60110
|
+
// packages/workit-core/src/core/methods.ts
|
|
60111
|
+
var METHODS = {
|
|
60112
|
+
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
60113
|
+
"workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
|
|
60114
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
60115
|
+
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
60116
|
+
"workit-implement": { dimensions: ["delegation"] },
|
|
60117
|
+
"workit-debug": {},
|
|
60118
|
+
"workit-handoff": {},
|
|
60119
|
+
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
59760
60120
|
};
|
|
59761
|
-
var
|
|
59762
|
-
|
|
59763
|
-
|
|
59764
|
-
|
|
59765
|
-
|
|
59766
|
-
|
|
59767
|
-
|
|
59768
|
-
|
|
59769
|
-
|
|
59770
|
-
|
|
59771
|
-
|
|
59772
|
-
}
|
|
59773
|
-
if (hasLocalBase) {
|
|
59774
|
-
run(["checkout", base]);
|
|
59775
|
-
try {
|
|
59776
|
-
run(["merge", "--ff-only", `origin/${base}`]);
|
|
59777
|
-
} catch {}
|
|
59778
|
-
} else {
|
|
59779
|
-
run(["checkout", "-b", base, "--track", `origin/${base}`]);
|
|
59780
|
-
}
|
|
59781
|
-
return { ok: true };
|
|
59782
|
-
} catch (error) {
|
|
60121
|
+
var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
60122
|
+
var assuranceFor = (id, definition, capabilities) => {
|
|
60123
|
+
const keys = new Set([
|
|
60124
|
+
id,
|
|
60125
|
+
...definition.dimensions ?? [],
|
|
60126
|
+
...definition.ruleIds ?? []
|
|
60127
|
+
]);
|
|
60128
|
+
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
60129
|
+
if (id === "workit-review" && matches.length === 0)
|
|
60130
|
+
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
60131
|
+
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
59783
60132
|
return {
|
|
59784
|
-
|
|
59785
|
-
|
|
60133
|
+
assurance: "unavailable",
|
|
60134
|
+
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
59786
60135
|
};
|
|
59787
|
-
|
|
59788
|
-
};
|
|
59789
|
-
|
|
59790
|
-
const git = gitContext(cwd);
|
|
59791
|
-
if (!git.branch || git.branch === "unknown")
|
|
59792
|
-
return { ok: false, error: "not in a git repository" };
|
|
59793
|
-
const ready = originBaseReady(cwd, base);
|
|
59794
|
-
if (!ready.ok)
|
|
59795
|
-
return ready;
|
|
59796
|
-
return fastForwardBase(cwd, base);
|
|
60136
|
+
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
60137
|
+
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
60138
|
+
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
59797
60139
|
};
|
|
59798
|
-
|
|
59799
|
-
|
|
59800
|
-
|
|
59801
|
-
|
|
59802
|
-
|
|
59803
|
-
|
|
59804
|
-
|
|
60140
|
+
function selectMethods(policy, capabilities) {
|
|
60141
|
+
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
60142
|
+
const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
|
|
60143
|
+
if (requirements.length === 0)
|
|
60144
|
+
return [];
|
|
60145
|
+
const capability = assuranceFor(id, definition, capabilities);
|
|
60146
|
+
const reasons = [
|
|
60147
|
+
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
60148
|
+
...capability.reasons
|
|
60149
|
+
];
|
|
60150
|
+
if (capability.assurance === "unavailable")
|
|
60151
|
+
reasons.unshift("required capability is unavailable");
|
|
60152
|
+
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
60153
|
+
});
|
|
60154
|
+
}
|
|
60155
|
+
|
|
60156
|
+
// packages/workit-core/src/core/task-context.ts
|
|
60157
|
+
function reconcileResume(view, observations = []) {
|
|
60158
|
+
if (observations.length > 0)
|
|
60159
|
+
return failure("permission_denied", "worker observations require native host verification");
|
|
60160
|
+
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
60161
|
+
if (!candidate.ok)
|
|
60162
|
+
return candidate;
|
|
60163
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
60164
|
+
const blockers = [...view.task.progress.blockers];
|
|
60165
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
60166
|
+
blockers.push({
|
|
60167
|
+
reason: "worker state requires reconciliation",
|
|
60168
|
+
dependentAction: "resume",
|
|
60169
|
+
refs: []
|
|
59805
60170
|
});
|
|
59806
|
-
|
|
59807
|
-
|
|
59808
|
-
|
|
59809
|
-
|
|
59810
|
-
|
|
59811
|
-
|
|
59812
|
-
|
|
59813
|
-
|
|
59814
|
-
|
|
59815
|
-
|
|
59816
|
-
|
|
59817
|
-
|
|
59818
|
-
|
|
59819
|
-
|
|
59820
|
-
|
|
59821
|
-
|
|
59822
|
-
|
|
59823
|
-
|
|
59824
|
-
|
|
59825
|
-
}
|
|
59826
|
-
|
|
59827
|
-
|
|
59828
|
-
var
|
|
59829
|
-
|
|
59830
|
-
sdd_dir,
|
|
59831
|
-
target_branch,
|
|
59832
|
-
stash,
|
|
59833
|
-
workspace_root,
|
|
59834
|
-
log
|
|
59835
|
-
}) => {
|
|
59836
|
-
const cwd = path23.resolve(workspace_root);
|
|
59837
|
-
const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
59838
|
-
const current = gitContext(cwd).branch;
|
|
59839
|
-
if (!current || current === "unknown")
|
|
59840
|
-
return { error: "not in a git repository", phase: "preflight" };
|
|
59841
|
-
const sdd = sdd_dir ?? "docs";
|
|
59842
|
-
const manifestPath = path23.isAbsolute(sdd) ? path23.join(sdd, "manifest.json") : path23.join(cwd, sdd, "manifest.json");
|
|
59843
|
-
const ensureManifestDir = () => {
|
|
59844
|
-
const dir = path23.dirname(manifestPath);
|
|
59845
|
-
if (!existsSync16(dir))
|
|
59846
|
-
mkdirSync8(dir, { recursive: true, mode: 493 });
|
|
59847
|
-
};
|
|
59848
|
-
const readManifest = () => {
|
|
59849
|
-
try {
|
|
59850
|
-
return JSON.parse(readFileSync16(manifestPath, "utf8"));
|
|
59851
|
-
} catch {
|
|
59852
|
-
return {};
|
|
59853
|
-
}
|
|
59854
|
-
};
|
|
59855
|
-
const writeManifest = (data) => {
|
|
59856
|
-
ensureManifestDir();
|
|
59857
|
-
writeFileSync10(manifestPath, JSON.stringify(data, null, 2) + `
|
|
59858
|
-
`, "utf8");
|
|
59859
|
-
};
|
|
59860
|
-
const journal = (message) => log?.(`branch-setup: ${message}`);
|
|
59861
|
-
if (action === "reapply_stash") {
|
|
59862
|
-
const manifest = readManifest();
|
|
59863
|
-
const ref = manifest.stash_ref;
|
|
59864
|
-
if (!ref)
|
|
59865
|
-
return { error: "no stash_ref in manifest", phase: "preflight" };
|
|
59866
|
-
journal(`pre-pop: ${String(ref)}`);
|
|
59867
|
-
try {
|
|
59868
|
-
exec(["stash", "pop", String(ref)]);
|
|
59869
|
-
} catch (error) {
|
|
59870
|
-
journal("pop: failed");
|
|
59871
|
-
return {
|
|
59872
|
-
error: error instanceof Error ? error.message : "stash pop failed",
|
|
59873
|
-
phase: "post_mutation"
|
|
59874
|
-
};
|
|
59875
|
-
}
|
|
59876
|
-
journal("pop: ok");
|
|
59877
|
-
delete manifest.stash_ref;
|
|
59878
|
-
delete manifest.stash_created_at;
|
|
59879
|
-
writeManifest(manifest);
|
|
59880
|
-
return { action: "reapply_stash", ok: true };
|
|
59881
|
-
}
|
|
59882
|
-
const target = target_branch ?? "";
|
|
59883
|
-
if (!target)
|
|
60171
|
+
return success(null, null, {
|
|
60172
|
+
candidate: candidate.data,
|
|
60173
|
+
staleEvidenceIds,
|
|
60174
|
+
workerUpdates: [],
|
|
60175
|
+
blockers,
|
|
60176
|
+
reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
|
|
60177
|
+
});
|
|
60178
|
+
}
|
|
60179
|
+
var COMPACT_MAX_BYTES = 4096;
|
|
60180
|
+
var COMPACT_MAX_ITEMS = 8;
|
|
60181
|
+
var COMPACT_TEXT_BYTES = 128;
|
|
60182
|
+
var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
|
|
60183
|
+
var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
|
|
60184
|
+
var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
|
|
60185
|
+
var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
|
|
60186
|
+
const separator = match.search(/[:=]/u);
|
|
60187
|
+
if (separator >= 0)
|
|
60188
|
+
return `${match.slice(0, separator)}=<redacted>`;
|
|
60189
|
+
const space = match.search(/\s/u);
|
|
60190
|
+
return `${match.slice(0, space)} <redacted>`;
|
|
60191
|
+
}).slice(0, limit);
|
|
60192
|
+
var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
60193
|
+
var compactReference = (ref) => {
|
|
60194
|
+
if (ref.kind === "file")
|
|
59884
60195
|
return {
|
|
59885
|
-
|
|
59886
|
-
|
|
60196
|
+
kind: "file",
|
|
60197
|
+
path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
|
|
59887
60198
|
};
|
|
59888
|
-
if (
|
|
59889
|
-
return { error: `protected branch ${target}`, phase: "preflight" };
|
|
59890
|
-
if (!allowedBranch(cwd, target))
|
|
60199
|
+
if (ref.kind === "record")
|
|
59891
60200
|
return {
|
|
59892
|
-
|
|
59893
|
-
|
|
60201
|
+
kind: "record",
|
|
60202
|
+
collection: ref.collection,
|
|
60203
|
+
id: ref.id
|
|
59894
60204
|
};
|
|
59895
|
-
|
|
59896
|
-
|
|
59897
|
-
|
|
59898
|
-
|
|
59899
|
-
|
|
59900
|
-
|
|
59901
|
-
|
|
59902
|
-
|
|
59903
|
-
|
|
59904
|
-
|
|
59905
|
-
|
|
59906
|
-
|
|
59907
|
-
|
|
59908
|
-
|
|
59909
|
-
|
|
59910
|
-
const
|
|
59911
|
-
|
|
59912
|
-
|
|
59913
|
-
|
|
59914
|
-
|
|
59915
|
-
|
|
59916
|
-
|
|
59917
|
-
|
|
59918
|
-
|
|
59919
|
-
|
|
59920
|
-
|
|
59921
|
-
|
|
59922
|
-
}
|
|
59923
|
-
return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
|
|
60205
|
+
return { kind: ref.kind };
|
|
60206
|
+
};
|
|
60207
|
+
function compactTaskContext(view) {
|
|
60208
|
+
const decisions = view.task.decisions.slice().sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
|
|
60209
|
+
id: entry.id,
|
|
60210
|
+
purpose: entry.data.purpose,
|
|
60211
|
+
status: entry.data.response,
|
|
60212
|
+
digest: entry.data.digest,
|
|
60213
|
+
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
60214
|
+
}));
|
|
60215
|
+
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
60216
|
+
id: method.id,
|
|
60217
|
+
assurance: method.assurance,
|
|
60218
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
60219
|
+
})) : [];
|
|
60220
|
+
const gaps = Array.from(new Set([
|
|
60221
|
+
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
60222
|
+
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
60223
|
+
...view.task.progress.blockers.map((entry) => entry.reason)
|
|
60224
|
+
])).sort().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
|
|
60225
|
+
const context = {
|
|
60226
|
+
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
60227
|
+
status: view.task.status,
|
|
60228
|
+
decisions,
|
|
60229
|
+
methods,
|
|
60230
|
+
gaps,
|
|
60231
|
+
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
59924
60232
|
};
|
|
59925
|
-
|
|
59926
|
-
|
|
59927
|
-
if (
|
|
59928
|
-
|
|
59929
|
-
|
|
59930
|
-
|
|
59931
|
-
|
|
59932
|
-
|
|
59933
|
-
|
|
59934
|
-
if (!targetExists && base !== undefined) {
|
|
59935
|
-
const ready = originBaseReady(cwd, base);
|
|
59936
|
-
if (!ready.ok)
|
|
59937
|
-
return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
|
|
59938
|
-
validatedBase = base;
|
|
59939
|
-
}
|
|
59940
|
-
if (dirty && stash === "yes") {
|
|
59941
|
-
try {
|
|
59942
|
-
exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
|
|
59943
|
-
} catch (error) {
|
|
59944
|
-
return {
|
|
59945
|
-
error: error instanceof Error ? error.message : "stash push failed",
|
|
59946
|
-
phase: "post_mutation"
|
|
59947
|
-
};
|
|
59948
|
-
}
|
|
59949
|
-
stash_ref = "stash@{0}";
|
|
59950
|
-
journal(`stash push: ${stash_ref}`);
|
|
59951
|
-
}
|
|
59952
|
-
try {
|
|
59953
|
-
exec(["checkout", target]);
|
|
59954
|
-
journal("post-checkout");
|
|
59955
|
-
} catch (error) {
|
|
59956
|
-
const message = error instanceof Error ? error.message : "checkout failed";
|
|
59957
|
-
if (/worktree/i.test(message)) {
|
|
59958
|
-
return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
|
|
59959
|
-
}
|
|
59960
|
-
try {
|
|
59961
|
-
let effectiveBase = base;
|
|
59962
|
-
if (effectiveBase === undefined) {
|
|
59963
|
-
const lateResolved = baseBranch(cwd);
|
|
59964
|
-
if ("error" in lateResolved)
|
|
59965
|
-
return failAfterStash(lateResolved.error, "preflight");
|
|
59966
|
-
effectiveBase = lateResolved.base;
|
|
59967
|
-
}
|
|
59968
|
-
const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
|
|
59969
|
-
if (!baseResult.ok)
|
|
59970
|
-
return failAfterStash(baseResult.error ?? "ensure-base-branch failed");
|
|
59971
|
-
exec(["checkout", "-b", target]);
|
|
59972
|
-
journal("post-create");
|
|
59973
|
-
} catch (createError) {
|
|
59974
|
-
return failAfterStash(createError instanceof Error ? createError.message : "branch create failed");
|
|
59975
|
-
}
|
|
59976
|
-
}
|
|
60233
|
+
let encoded = JSON.stringify(context);
|
|
60234
|
+
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
60235
|
+
if (context.gaps.length)
|
|
60236
|
+
context.gaps.pop();
|
|
60237
|
+
else if (context.decisions.length)
|
|
60238
|
+
context.decisions.pop();
|
|
60239
|
+
else
|
|
60240
|
+
context.methods.pop();
|
|
60241
|
+
encoded = JSON.stringify(context);
|
|
59977
60242
|
}
|
|
59978
|
-
|
|
59979
|
-
|
|
59980
|
-
|
|
59981
|
-
|
|
59982
|
-
if (stash_ref) {
|
|
59983
|
-
manifest.stash_ref = stash_ref;
|
|
59984
|
-
manifest.stash_created_at = new Date().toISOString();
|
|
59985
|
-
}
|
|
59986
|
-
writeManifest(manifest);
|
|
59987
|
-
} catch (error) {
|
|
59988
|
-
const result = failAfterStash(error instanceof Error ? `manifest update failed: ${error.message}` : "manifest update failed");
|
|
59989
|
-
if (stash_ref === undefined && gitContext(cwd).branch !== current) {
|
|
59990
|
-
try {
|
|
59991
|
-
exec(["checkout", current]);
|
|
59992
|
-
} catch {
|
|
59993
|
-
try {
|
|
59994
|
-
exec(["checkout", "-m", current]);
|
|
59995
|
-
} catch {}
|
|
59996
|
-
}
|
|
59997
|
-
}
|
|
59998
|
-
return result;
|
|
60243
|
+
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
60244
|
+
context.objective = compactText(context.objective, 64) ?? "";
|
|
60245
|
+
context.nextAction = compactText(context.nextAction, 64);
|
|
60246
|
+
encoded = JSON.stringify(context);
|
|
59999
60247
|
}
|
|
60000
|
-
return
|
|
60001
|
-
|
|
60002
|
-
|
|
60003
|
-
|
|
60004
|
-
|
|
60005
|
-
|
|
60006
|
-
|
|
60007
|
-
|
|
60008
|
-
};
|
|
60248
|
+
return encoded;
|
|
60249
|
+
}
|
|
60250
|
+
var exportDigest = (bundle) => sha256({
|
|
60251
|
+
schemaVersion: bundle.schemaVersion,
|
|
60252
|
+
exportedAt: bundle.exportedAt,
|
|
60253
|
+
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
60254
|
+
task: bundle.task
|
|
60255
|
+
});
|
|
60009
60256
|
// packages/workit-core/src/core/external-action.ts
|
|
60010
60257
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
60011
60258
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
@@ -60040,6 +60287,13 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
60040
60287
|
babysit: boolean2().optional()
|
|
60041
60288
|
}).strict()
|
|
60042
60289
|
}).strict(),
|
|
60290
|
+
object({
|
|
60291
|
+
operation: literal("hosting.merge"),
|
|
60292
|
+
payload: object({
|
|
60293
|
+
target_branch: string2().min(1).optional(),
|
|
60294
|
+
source_branch: string2().min(1).optional()
|
|
60295
|
+
}).strict()
|
|
60296
|
+
}).strict(),
|
|
60043
60297
|
object({
|
|
60044
60298
|
operation: literal("youtrack.update"),
|
|
60045
60299
|
payload: object({
|
|
@@ -60113,7 +60367,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
|
|
|
60113
60367
|
payload,
|
|
60114
60368
|
...payload && typeof payload === "object" && !Array.isArray(payload) && "resolved" in payload && payload.resolved && typeof payload.resolved === "object" && Array.isArray(payload.resolved.steps) ? { steps: payload.resolved.steps } : {}
|
|
60115
60369
|
});
|
|
60116
|
-
var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch(required unless reapply_stash; the working branch to create or switch to),stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
|
|
60370
|
+
var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch(required unless reapply_stash; the working branch to create or switch to),stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; hosting.merge {target_branch?,source_branch?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
|
|
60117
60371
|
var externalActionRef = (host, actor, descriptor) => ({
|
|
60118
60372
|
kind: "host",
|
|
60119
60373
|
host,
|
|
@@ -61620,7 +61874,10 @@ class WorkitCore {
|
|
|
61620
61874
|
observeDecision(request, observation) {
|
|
61621
61875
|
return this.recordDecision(request, observation, true);
|
|
61622
61876
|
}
|
|
61623
|
-
|
|
61877
|
+
observeStandingDecision(request) {
|
|
61878
|
+
return this.recordDecision(request, undefined, true, true);
|
|
61879
|
+
}
|
|
61880
|
+
recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
|
|
61624
61881
|
const root = this.contextRootError();
|
|
61625
61882
|
if (!root.ok)
|
|
61626
61883
|
return root;
|
|
@@ -61635,8 +61892,10 @@ class WorkitCore {
|
|
|
61635
61892
|
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
61636
61893
|
if (stated && !input.binding?.statedChoice)
|
|
61637
61894
|
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
61638
|
-
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
61895
|
+
if (nativeRequired && nativeObservation === undefined && !stated && !standing)
|
|
61639
61896
|
return failure("permission_denied", "native decision observation is required");
|
|
61897
|
+
if (standing && (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved"))
|
|
61898
|
+
return failure("invalid_input", "standing approvals need an approved action binding with a standing rule");
|
|
61640
61899
|
const task = this.store.readTask(input.taskId);
|
|
61641
61900
|
if (!task.ok)
|
|
61642
61901
|
return task;
|
|
@@ -61668,7 +61927,10 @@ class WorkitCore {
|
|
|
61668
61927
|
consumption: null
|
|
61669
61928
|
};
|
|
61670
61929
|
const data = { ...base, digest: decisionDigest(base) };
|
|
61671
|
-
const
|
|
61930
|
+
const standingApproval = standing && !stated ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding) : null;
|
|
61931
|
+
if (standingApproval && !standingApproval.ok)
|
|
61932
|
+
return standingApproval;
|
|
61933
|
+
const native = nativeRequired && !stated && !standingApproval ? verifyNativeDecision(this.context.nativeAuthority, {
|
|
61672
61934
|
observation: nativeObservation,
|
|
61673
61935
|
expected: {
|
|
61674
61936
|
taskId: task.data.id,
|
|
@@ -61699,7 +61961,7 @@ class WorkitCore {
|
|
|
61699
61961
|
const entry = {
|
|
61700
61962
|
id: newId(),
|
|
61701
61963
|
recordedAt: mutation.now,
|
|
61702
|
-
provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
61964
|
+
provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
61703
61965
|
data
|
|
61704
61966
|
};
|
|
61705
61967
|
return success(mutation.revision, null, {
|
|
@@ -63120,10 +63382,58 @@ function prCreate(env, cwd) {
|
|
|
63120
63382
|
output: (result.stdout ?? "").trim()
|
|
63121
63383
|
};
|
|
63122
63384
|
}
|
|
63385
|
+
function mergePr(cwd, opts) {
|
|
63386
|
+
const root = repoRoot2(cwd);
|
|
63387
|
+
const cfg = vcsConfig("load", root);
|
|
63388
|
+
if (!cfg.ok)
|
|
63389
|
+
return { error: cfg.error ?? "vcs config missing" };
|
|
63390
|
+
if (!cfg.tokenReady)
|
|
63391
|
+
return { error: "VCS token not ready — run /wk-init and edit token file locally" };
|
|
63392
|
+
const provider = cfg.provider;
|
|
63393
|
+
if (provider !== "gitlab" && provider !== "github")
|
|
63394
|
+
return { error: `unsupported provider: ${provider}` };
|
|
63395
|
+
if (!hostingCliAvailable(provider))
|
|
63396
|
+
return { error: `workflow CLI missing (required for ${provider})` };
|
|
63397
|
+
const pr = cfg.pr ?? {};
|
|
63398
|
+
const token = fs11.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
63399
|
+
const squash = pr.squashOnMerge !== false;
|
|
63400
|
+
const removeBranch = pr.removeSourceBranch !== false;
|
|
63401
|
+
let cmd;
|
|
63402
|
+
let cmdEnv;
|
|
63403
|
+
if (provider === "gitlab") {
|
|
63404
|
+
cmd = ["glab", "mr", "merge", opts.source];
|
|
63405
|
+
cmd.push(squash ? "--squash" : "--squash=false");
|
|
63406
|
+
if (removeBranch)
|
|
63407
|
+
cmd.push("--remove-source-branch");
|
|
63408
|
+
cmd.push("--yes");
|
|
63409
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GITLAB_TOKEN: token };
|
|
63410
|
+
} else {
|
|
63411
|
+
cmd = ["gh", "pr", "merge", opts.source];
|
|
63412
|
+
cmd.push(squash ? "--squash" : "--merge");
|
|
63413
|
+
if (removeBranch)
|
|
63414
|
+
cmd.push("--delete-branch");
|
|
63415
|
+
cmd.push("--yes");
|
|
63416
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GH_TOKEN: token };
|
|
63417
|
+
}
|
|
63418
|
+
const result = spawnSync6(cmd[0], cmd.slice(1), { cwd: root, encoding: "utf8", env: cmdEnv });
|
|
63419
|
+
if (result.status !== 0)
|
|
63420
|
+
return {
|
|
63421
|
+
error: "merge failed",
|
|
63422
|
+
provider,
|
|
63423
|
+
stderr: (result.stderr ?? result.stdout ?? "").trim().slice(0, 800)
|
|
63424
|
+
};
|
|
63425
|
+
return {
|
|
63426
|
+
ok: true,
|
|
63427
|
+
provider,
|
|
63428
|
+
targetBranch: opts.target,
|
|
63429
|
+
sourceBranch: opts.source,
|
|
63430
|
+
output: (result.stdout ?? "").trim()
|
|
63431
|
+
};
|
|
63432
|
+
}
|
|
63123
63433
|
|
|
63124
63434
|
// packages/workit-core/src/core/repo-context.ts
|
|
63125
63435
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
63126
|
-
import { existsSync as existsSync18, readFileSync as
|
|
63436
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18, readdirSync as readdirSync9 } from "node:fs";
|
|
63127
63437
|
import path26 from "node:path";
|
|
63128
63438
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
63129
63439
|
try {
|
|
@@ -63293,7 +63603,7 @@ var packageScripts = (cwd, keys) => {
|
|
|
63293
63603
|
if (!existsSync18(pkgPath))
|
|
63294
63604
|
return [];
|
|
63295
63605
|
try {
|
|
63296
|
-
const pkg = JSON.parse(
|
|
63606
|
+
const pkg = JSON.parse(readFileSync18(pkgPath, "utf8"));
|
|
63297
63607
|
return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
|
|
63298
63608
|
} catch {
|
|
63299
63609
|
return [];
|
|
@@ -63327,7 +63637,7 @@ var documentationFiles = (cwd) => {
|
|
|
63327
63637
|
var readTrimmed = (file, maxLines) => {
|
|
63328
63638
|
if (!existsSync18(file))
|
|
63329
63639
|
return "";
|
|
63330
|
-
const lines =
|
|
63640
|
+
const lines = readFileSync18(file, "utf8").split(`
|
|
63331
63641
|
`);
|
|
63332
63642
|
return lines.slice(0, maxLines).join(`
|
|
63333
63643
|
`);
|
|
@@ -63515,7 +63825,7 @@ function docsRefreshContext(root, range) {
|
|
|
63515
63825
|
const pkgPath = path26.join(cwd, "package.json");
|
|
63516
63826
|
if (existsSync18(pkgPath)) {
|
|
63517
63827
|
try {
|
|
63518
|
-
const pkg = JSON.parse(
|
|
63828
|
+
const pkg = JSON.parse(readFileSync18(pkgPath, "utf8"));
|
|
63519
63829
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
63520
63830
|
`;
|
|
63521
63831
|
} catch {}
|
|
@@ -64711,6 +65021,11 @@ var actionProposalQuestion = (request, descriptorPayload) => {
|
|
|
64711
65021
|
presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
|
|
64712
65022
|
approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
|
|
64713
65023
|
};
|
|
65024
|
+
case "hosting.merge":
|
|
65025
|
+
return {
|
|
65026
|
+
presented: `Workit decision: action — Merge \`${String(resolved.source_branch ?? "")}\` into \`${payloadTarget}\` (squash, delete branch)?`,
|
|
65027
|
+
approvedText: `Merge ${String(resolved.source_branch ?? "")} → ${payloadTarget}.`
|
|
65028
|
+
};
|
|
64714
65029
|
case "changelog.apply":
|
|
64715
65030
|
return {
|
|
64716
65031
|
presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
|
|
@@ -64741,7 +65056,7 @@ var actionProposalQuestion = (request, descriptorPayload) => {
|
|
|
64741
65056
|
var approvedResolvedExternalAction = (content) => {
|
|
64742
65057
|
try {
|
|
64743
65058
|
const value = JSON.parse(content);
|
|
64744
|
-
if (value.operation !== "hosting.pull_request" && value.operation !== "changelog.apply" && value.operation !== "youtrack.update" && value.operation !== "youtrack.time" && value.operation !== "youtrack.meeting")
|
|
65059
|
+
if (value.operation !== "hosting.pull_request" && value.operation !== "hosting.merge" && value.operation !== "changelog.apply" && value.operation !== "youtrack.update" && value.operation !== "youtrack.time" && value.operation !== "youtrack.meeting")
|
|
64745
65060
|
return failure("invalid_input", "approved action is not a reconciliable external action");
|
|
64746
65061
|
if (!value.payload || typeof value.payload !== "object")
|
|
64747
65062
|
return failure("invalid_input", "approved action payload is incomplete");
|
|
@@ -64754,6 +65069,8 @@ var approvedResolvedExternalAction = (content) => {
|
|
|
64754
65069
|
const target = resolved;
|
|
64755
65070
|
if (value.operation === "hosting.pull_request" && (typeof target.marker !== "string" || typeof target.source_branch !== "string" || typeof target.source_commit !== "string" || typeof target.remote !== "string" || typeof payload.target_branch !== "string"))
|
|
64756
65071
|
return failure("invalid_input", "approved hosting target is incomplete");
|
|
65072
|
+
if (value.operation === "hosting.merge" && (typeof target.marker !== "string" || typeof target.source_branch !== "string" || typeof target.source_commit !== "string" || typeof target.remote !== "string" || typeof payload.target_branch !== "string"))
|
|
65073
|
+
return failure("invalid_input", "approved hosting target is incomplete");
|
|
64757
65074
|
if (value.operation === "changelog.apply" && (typeof target.target !== "string" || typeof target.beforeDigest !== "string" || typeof target.beforeExists !== "boolean" || typeof target.afterDigest !== "string"))
|
|
64758
65075
|
return failure("invalid_input", "approved changelog target is incomplete");
|
|
64759
65076
|
if (value.operation !== "hosting.pull_request" && value.operation !== "changelog.apply" && (typeof target.marker !== "string" || typeof target.baseUrl !== "string" || typeof target.issueId !== "string" || !Array.isArray(target.steps)))
|
|
@@ -65045,6 +65362,11 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
65045
65362
|
}
|
|
65046
65363
|
case "git.push": {
|
|
65047
65364
|
const branch = request.payload.branch ?? gitValue(root, ["branch", "--show-current"]);
|
|
65365
|
+
if (branch && isProtectedBranch2(root, branch))
|
|
65366
|
+
return failure("invalid_input", `cannot push protected branch ${branch}`, {
|
|
65367
|
+
outcome: "not_started",
|
|
65368
|
+
fields: [{ path: "branch", reason: "protected branch" }]
|
|
65369
|
+
});
|
|
65048
65370
|
const commit = branch ? gitValue(root, ["rev-parse", branch]) : null;
|
|
65049
65371
|
const remote = pushRemote(root);
|
|
65050
65372
|
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
@@ -65142,6 +65464,11 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
65142
65464
|
case "hosting.pull_request": {
|
|
65143
65465
|
const target_branch = request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
65144
65466
|
const source_branch = gitValue(root, ["branch", "--show-current"]);
|
|
65467
|
+
if (source_branch && isProtectedBranch2(root, source_branch))
|
|
65468
|
+
return failure("invalid_input", `cannot open a PR from protected branch ${source_branch}`, {
|
|
65469
|
+
outcome: "not_started",
|
|
65470
|
+
fields: [{ path: "source_branch", reason: "protected branch" }]
|
|
65471
|
+
});
|
|
65145
65472
|
const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
|
|
65146
65473
|
const remote = pushRemote(root);
|
|
65147
65474
|
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
@@ -65173,6 +65500,44 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
|
|
|
65173
65500
|
}
|
|
65174
65501
|
});
|
|
65175
65502
|
}
|
|
65503
|
+
case "hosting.merge": {
|
|
65504
|
+
const target_branch = request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
65505
|
+
const source_branch = request.payload.source_branch ?? gitValue(root, ["branch", "--show-current"]);
|
|
65506
|
+
if (source_branch && isProtectedBranch2(root, source_branch))
|
|
65507
|
+
return failure("invalid_input", `cannot merge protected branch ${source_branch}`, {
|
|
65508
|
+
outcome: "not_started",
|
|
65509
|
+
fields: [{ path: "source_branch", reason: "protected branch" }]
|
|
65510
|
+
});
|
|
65511
|
+
const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
|
|
65512
|
+
const remote = pushRemote(root);
|
|
65513
|
+
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
65514
|
+
if (!source_branch || !source_commit || !identity || remoteProvider2(identity) !== null && remoteProvider2(identity) !== String(vcsConfig("load", root).provider).toLowerCase())
|
|
65515
|
+
return failure("capability_unavailable", "hosting target is not bound to the configured remote provider", { capability: "hosting.merge" });
|
|
65516
|
+
const vcs = vcsConfig("load", root);
|
|
65517
|
+
if (!vcs.ok || !vcs.tokenReady)
|
|
65518
|
+
return failure("capability_unavailable", "hosting credentials are unavailable", {
|
|
65519
|
+
capability: "hosting.merge",
|
|
65520
|
+
outcome: "not_started"
|
|
65521
|
+
});
|
|
65522
|
+
if (!hostingCliAvailable(String(vcs.provider)))
|
|
65523
|
+
return failure("capability_unavailable", "hosting CLI is unavailable", {
|
|
65524
|
+
capability: "hosting.merge",
|
|
65525
|
+
outcome: "not_started"
|
|
65526
|
+
});
|
|
65527
|
+
const normalized = {
|
|
65528
|
+
operation: request.operation,
|
|
65529
|
+
payload: { ...request.payload, target_branch, source_branch }
|
|
65530
|
+
};
|
|
65531
|
+
const marker = `<!-- workit-action:${sha256({ operation: request.operation, payload: normalized.payload, source_commit, remote: identity })} -->`;
|
|
65532
|
+
return success(null, null, {
|
|
65533
|
+
request: normalized,
|
|
65534
|
+
marker,
|
|
65535
|
+
descriptorPayload: {
|
|
65536
|
+
...normalized.payload,
|
|
65537
|
+
resolved: { source_branch, source_commit, remote: identity, target_branch, marker }
|
|
65538
|
+
}
|
|
65539
|
+
});
|
|
65540
|
+
}
|
|
65176
65541
|
case "changelog.apply": {
|
|
65177
65542
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
65178
65543
|
if (preview.error)
|
|
@@ -65348,6 +65713,17 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
|
|
|
65348
65713
|
...request.payload.babysit ?? true ? { next: prBabysitNext(String(result.output ?? "")) } : {}
|
|
65349
65714
|
});
|
|
65350
65715
|
}
|
|
65716
|
+
case "hosting.merge": {
|
|
65717
|
+
const target = String(request.payload.target_branch ?? "");
|
|
65718
|
+
const source = String(request.payload.source_branch ?? "");
|
|
65719
|
+
if (!target || !source)
|
|
65720
|
+
return failure("invalid_input", "hosting.merge requires target and source branches", {
|
|
65721
|
+
outcome: "not_started",
|
|
65722
|
+
fields: [{ path: "target_branch", reason: "required" }]
|
|
65723
|
+
});
|
|
65724
|
+
const result = mergePr(root, { target, source });
|
|
65725
|
+
return result.error || result.ok === false ? unknown2(request.operation) : success(null, null, result);
|
|
65726
|
+
}
|
|
65351
65727
|
case "changelog.apply": {
|
|
65352
65728
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
65353
65729
|
if (preview.error || approvedBeforeDigest !== undefined && preview.beforeDigest !== approvedBeforeDigest || approvedBeforeExists !== undefined && preview.beforeExists !== approvedBeforeExists)
|
|
@@ -65472,7 +65848,7 @@ async function payloadValue(raw, deps) {
|
|
|
65472
65848
|
return { ok: true, value: {} };
|
|
65473
65849
|
let source;
|
|
65474
65850
|
try {
|
|
65475
|
-
source = raw === "-" ? await readInput(deps.stdin ?? process.stdin) : raw.startsWith("@") ? decodeUtf8(
|
|
65851
|
+
source = raw === "-" ? await readInput(deps.stdin ?? process.stdin) : raw.startsWith("@") ? decodeUtf8(readFileSync19(raw.slice(1))) : raw;
|
|
65476
65852
|
} catch (error) {
|
|
65477
65853
|
return {
|
|
65478
65854
|
ok: false,
|
|
@@ -65815,7 +66191,7 @@ var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedEx
|
|
|
65815
66191
|
const store = new TaskStore(root);
|
|
65816
66192
|
const approved = approvedExternalAction(store, "workit_cli", actor, operationValue);
|
|
65817
66193
|
if (!approved.ok) {
|
|
65818
|
-
const plan = planCommitBinding(store, "workit_cli", actor, operationValue) ?? chainStepBinding(store, "workit_cli", actor, operationValue);
|
|
66194
|
+
const plan = planCommitBinding(store, "workit_cli", actor, operationValue) ?? chainStepBinding(store, "workit_cli", actor, operationValue) ?? standingAutoBinding(core, store, "workit_cli", actor, operationValue);
|
|
65819
66195
|
if (plan) {
|
|
65820
66196
|
const actionRef = externalActionRef("workit_cli", actor, operationValue);
|
|
65821
66197
|
return {
|
|
@@ -65944,7 +66320,10 @@ async function runActionCommand(argv, deps = {}) {
|
|
|
65944
66320
|
return result.ok ? 0 : 1;
|
|
65945
66321
|
}
|
|
65946
66322
|
const tty = deps.stdinIsTTY ? deps.stdinIsTTY() : process.stdin.isTTY === true;
|
|
65947
|
-
|
|
66323
|
+
const actor = deps.actor ?? "cli";
|
|
66324
|
+
const store = new TaskStore(root);
|
|
66325
|
+
const autoApplies = standingAutoApplies(store, "workit_cli", actor, descriptor);
|
|
66326
|
+
if ((!tty || !argv.includes("--confirm")) && !autoApplies) {
|
|
65948
66327
|
const result = failure("needs_input", "TTY confirmation is required for external actions", {
|
|
65949
66328
|
operation: normalized.operation
|
|
65950
66329
|
});
|
|
@@ -65954,8 +66333,6 @@ async function runActionCommand(argv, deps = {}) {
|
|
|
65954
66333
|
printHuman(result, deps);
|
|
65955
66334
|
return 1;
|
|
65956
66335
|
}
|
|
65957
|
-
const actor = deps.actor ?? "cli";
|
|
65958
|
-
const store = new TaskStore(root);
|
|
65959
66336
|
const taskId = taskIndex >= 0 ? argv[taskIndex + 1] : undefined;
|
|
65960
66337
|
const listed = store.listTasks();
|
|
65961
66338
|
const workspace = store.readWorkspace();
|
|
@@ -66059,6 +66436,7 @@ async function runActionCommand(argv, deps = {}) {
|
|
|
66059
66436
|
"git.commit",
|
|
66060
66437
|
"git.push",
|
|
66061
66438
|
"hosting.pull_request",
|
|
66439
|
+
"hosting.merge",
|
|
66062
66440
|
"changelog.apply"
|
|
66063
66441
|
].includes(normalized.operation);
|
|
66064
66442
|
if (localOperation) {
|
|
@@ -66094,6 +66472,20 @@ async function runActionCommand(argv, deps = {}) {
|
|
|
66094
66472
|
printHuman(result, deps);
|
|
66095
66473
|
return result.ok ? 0 : 1;
|
|
66096
66474
|
}
|
|
66475
|
+
if (standingAutoApplies(store, "workit_cli", actor, descriptor)) {
|
|
66476
|
+
const core = new WorkitCore(store, {
|
|
66477
|
+
...contextFor(root, { ...deps, actor }, "host_observed"),
|
|
66478
|
+
nativeAuthority: cliActionAuthority(actor),
|
|
66479
|
+
workerId: null
|
|
66480
|
+
});
|
|
66481
|
+
const runner = nativeExternalActionRunner(root, actor, core);
|
|
66482
|
+
const result = await runner(descriptor, (step) => executeResolvedExternalAction(resolved.data, root, step, { host: "workit_cli", actor }));
|
|
66483
|
+
if (json)
|
|
66484
|
+
jsonResult(outOf(deps), result);
|
|
66485
|
+
else
|
|
66486
|
+
printHuman(result, deps);
|
|
66487
|
+
return result.ok ? 0 : 1;
|
|
66488
|
+
}
|
|
66097
66489
|
if (!json)
|
|
66098
66490
|
write2(outOf(deps), question.presented);
|
|
66099
66491
|
const accepted = await observeConsent(deps);
|