@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.
Files changed (2) hide show
  1. package/dist/index.js +2018 -1626
  2. 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 readFileSync18 } from "node:fs";
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 { spawnSync as spawnSync3 } from "node:child_process";
58291
- import path21 from "node:path";
58303
+ import path22 from "node:path";
58292
58304
 
58293
- // packages/workit-core/src/core/authority.ts
58305
+ // packages/workit-core/src/core/auto-approval.ts
58294
58306
  import { createHash as createHash6 } from "node:crypto";
58295
- import * as fs8 from "node:fs";
58296
- import path20 from "node:path";
58297
- var verifiedAuthorities = new WeakMap;
58298
- var trustedAuthority = (kind, provenance, expected, binding, caller) => {
58299
- const token = {};
58300
- verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
58301
- return token;
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 chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
58304
- var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
58305
- var normalizeChainSteps = (steps) => {
58306
- if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
58307
- return null;
58308
- const normalized = [];
58309
- for (const step of steps) {
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
- if (step.pr === true && Object.keys(step).length === 1) {
58323
- normalized.push({ kind: "pr" });
58324
- continue;
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 keys = normalized.map(chainStepKey);
58329
- if (new Set(keys).size !== keys.length)
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
- return normalized;
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 scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
58334
- var parseSteps = (content) => {
58335
- let value;
58400
+ var readVcsConfig = () => {
58401
+ const file = vcsConfigPath();
58402
+ let raw;
58336
58403
  try {
58337
- value = JSON.parse(content);
58404
+ raw = fs8.readFileSync(file, "utf8");
58338
58405
  } catch {
58339
- return [];
58406
+ return { status: "missing", path: file, config: {} };
58340
58407
  }
58341
- if (typeof value !== "object" || value === null || !("steps" in value))
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
- root = fs8.realpathSync(checkoutRoot);
58410
+ parsed = JSON.parse(raw);
58448
58411
  } catch {
58449
- return failure("permission_denied", "checkout root is unavailable");
58412
+ return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
58450
58413
  }
58451
- for (const reference of binding.contentRefs) {
58452
- if (reference.kind !== "file")
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 success(null, null, null);
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 reserveAction(input) {
58548
- if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
58549
- return failure("invalid_input", "action reservation preconditions are required");
58550
- const authority = takeAuthority(input.authority, {
58551
- owner: input.authorityOwner,
58552
- store: input.store,
58553
- root: input.store.root
58554
- }, input.authorityCaller);
58555
- if (!authority)
58556
- return failure("permission_denied", "native reservation authority is not verified");
58557
- const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
58558
- const workspace = input.store.readWorkspace();
58559
- if (!workspace.ok)
58560
- return workspace;
58561
- if (!workspace.data)
58562
- return failure("not_found", "workspace not found");
58563
- if (workspace.data.revision !== input.expectedWorkspaceRevision)
58564
- return failure("revision_conflict", "workspace revision does not match", {
58565
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
58566
- actualWorkspaceRevision: workspace.data.revision
58567
- });
58568
- const valid = validateAction(input.store, current, workspace.data.id, input, authority);
58569
- if (!valid.ok)
58570
- return valid;
58571
- const { entry, workflow } = valid.data;
58572
- const existing = entry.data.consumption;
58573
- if (existing?.state === "uncertain")
58574
- return failure("external_outcome_unknown", "previous action outcome is unknown", {
58575
- operation: "reserve_action",
58576
- outcome: "unknown"
58577
- });
58578
- if (existing?.state === "consumed")
58579
- return failure("permission_denied", "action approval is consumed");
58580
- if (existing?.state === "reserved" && workflow.completed.length === 0)
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
- const actionProgress = workflow.steps.length ? [
58592
- ...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
58593
- {
58594
- decisionId: input.decisionId,
58595
- steps: [...workflow.steps],
58596
- completedSteps: [...workflow.completed]
58597
- }
58598
- ] : current.actionProgress ?? [];
58599
- return success(mutation.revision, null, {
58600
- ...current,
58601
- actionProgress,
58602
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
58603
- });
58604
- }, input.native?.now);
58605
- if (!changed.ok)
58606
- return changed;
58607
- const workspace = input.store.readWorkspace();
58608
- if (!workspace.ok || !workspace.data)
58609
- return failure("recovery_required", "workspace disappeared after reservation");
58610
- const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
58611
- if (!entry)
58612
- return failure("recovery_required", "reserved decision disappeared");
58613
- const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
58614
- steps: [],
58615
- completedSteps: []
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
- return success(changed.data.revision, workspace.data.revision, {
58618
- taskId: input.taskId,
58619
- decisionId: input.decisionId,
58620
- actionRef: input.actionRef,
58621
- taskRevision: changed.data.revision,
58622
- workspaceRevision: workspace.data.revision,
58623
- completedSteps: [...workflow.completedSteps],
58624
- remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
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 settleAction(input) {
58628
- if (!input.store || !input.taskRevision || !input.workspaceRevision)
58629
- return failure("invalid_input", "action settlement preconditions are required");
58630
- const authority = takeAuthority(input.authority, {
58631
- owner: input.authorityOwner,
58632
- store: input.store,
58633
- root: input.store.root
58634
- }, input.authorityCaller);
58635
- if (!authority)
58636
- return failure("permission_denied", "native settlement authority is not verified");
58637
- if (!refSchema.safeParse(input.actionRef).success)
58638
- return failure("invalid_input", "action reference is invalid");
58639
- if (!authorityMatches(authority, {
58640
- taskId: input.taskId,
58641
- workspaceId: authority.workspaceId,
58642
- decisionId: input.decisionId,
58643
- actionRef: input.actionRef,
58644
- taskRevision: input.taskRevision,
58645
- workspaceRevision: input.workspaceRevision,
58646
- outcome: input.outcome
58647
- }))
58648
- return failure("permission_denied", "native settlement authority is not verified");
58649
- let outcomeResult = null;
58650
- const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
58651
- const workspace = input.store.readWorkspace();
58652
- if (!workspace.ok)
58653
- return workspace;
58654
- if (!workspace.data)
58655
- return failure("not_found", "workspace not found");
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 (input.outcome === "not_started") {
58711
- const released = { ...entry.data, consumption: null };
58712
- return success(mutation.revision, null, {
58713
- ...current,
58714
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
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
- if (currentStep)
58718
- workflow.completed.push(currentStep);
58719
- const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
58720
- const settled = {
58721
- ...entry.data,
58722
- consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
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
- return success(mutation.revision, null, next);
58744
- }, input.native?.now);
58745
- if (!changed.ok)
58746
- return changed;
58747
- const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
58748
- if (!entry)
58749
- return failure("recovery_required", "settled decision disappeared");
58750
- if (outcomeResult)
58751
- return outcomeResult;
58752
- return success(changed.data.revision, input.workspaceRevision, entry);
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
- function reconcileAction(input) {
58755
- if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
58756
- return failure("invalid_input", "action reconciliation preconditions are required");
58757
- const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
58758
- if (!authority || authority.kind !== "reconciliation")
58759
- return failure("permission_denied", "native reconciliation authority is not verified");
58760
- 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)
58761
- return failure("permission_denied", "native reconciliation authority is not bound");
58762
- const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
58763
- const workspace = input.store.readWorkspace();
58764
- if (!workspace.ok)
58765
- return workspace;
58766
- if (!workspace.data)
58767
- return failure("not_found", "workspace not found");
58768
- if (authority.workspaceId !== workspace.data.id)
58769
- return failure("permission_denied", "native reconciliation workspace does not match");
58770
- if (workspace.data.revision !== input.workspaceRevision)
58771
- return failure("revision_conflict", "workspace revision does not match");
58772
- const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
58773
- if (!entry || entry.data.purpose !== "action")
58774
- return failure("not_found", "decision not found");
58775
- if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
58776
- return failure("permission_denied", "native reconciliation caller does not match approval");
58777
- 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)
58778
- return failure("permission_denied", "reconciliation decision binding is invalid");
58779
- if (entry.data.consumption?.state !== "uncertain")
58780
- return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
58781
- if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
58782
- return failure("permission_denied", "reconciliation reference does not match reservation");
58783
- const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
58784
- const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
58785
- const currentStep = input.step ?? workflow.steps[workflow.completed.length];
58786
- if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
58787
- return failure("permission_denied", "reconciliation step is outside the approved workflow");
58788
- if (input.outcome === "not_started") {
58789
- return success(mutation.revision, null, {
58790
- ...current,
58791
- decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
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
- if (currentStep)
58795
- workflow.completed.push(currentStep);
58796
- const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
58797
- const nextDecision = {
58798
- ...entry.data,
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
- const actionProgress = workflow.steps.length ? [
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 canonicalScope = (scope) => {
58838
- const paths = scope.paths.length ? scope.paths : ["."];
58839
- const normalizedPaths = paths.map(canonicalPath);
58840
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
58841
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
58842
- return null;
58843
- return {
58844
- description: scope.description,
58845
- paths: [...new Set(normalizedPaths)],
58846
- exclusions: [...new Set(normalizedExclusions)]
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 excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
58850
- var scopeMatches = (value, scope) => {
58851
- const normalized = canonicalScope(scope);
58852
- if (!normalized)
58853
- return false;
58854
- const paths = normalized.paths;
58855
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
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 pathRelevant = (value, scope) => scopeMatches(value, scope);
58858
- var compareCodeUnits2 = (left, right) => {
58859
- const length = Math.min(left.length, right.length);
58860
- for (let index = 0;index < length; index += 1) {
58861
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
58862
- if (difference !== 0)
58863
- return difference;
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
- return left.length - right.length;
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 gitPaths = (root) => {
58868
- const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
58869
- cwd: root,
58870
- encoding: "buffer"
58871
- });
58872
- if (result.status !== 0) {
58873
- const stderr = result.stderr?.toString("utf8") ?? "";
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
- paths: [],
58876
- uncertain: !/not a git repository/i.test(stderr),
58877
- git: /not a git repository/i.test(stderr) ? false : true
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
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path21.sep).join("/")) : [];
58881
- const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
58882
- if (stagedDeleted.status !== 0) {
58883
- const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
58884
- if (!/not a git repository/i.test(stderr))
58885
- return { paths, uncertain: true, git: true };
58886
- } else if (stagedDeleted.stdout) {
58887
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path21.sep).join("/")));
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 { paths: [...new Set(paths)], uncertain: false, git: true };
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
- var walk = (root, directory, output) => {
58892
- let names;
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
- names = fs9.readdirSync(directory);
58890
+ const raw = readFileSync15(`${configDirPath}/workspaces.json`, "utf8");
58891
+ return createHash6("sha256").update(raw).digest("hex");
58895
58892
  } catch {
58896
- return true;
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 scopeRoots = (root, scope) => {
58918
- const roots = [];
58919
- for (const item of scope.paths.length ? scope.paths : ["."]) {
58920
- const target = path21.resolve(root, item);
58921
- if (!inside(root, target))
58922
- 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` }] });
58923
- let ancestor = target;
58924
- while (!fs9.existsSync(ancestor) && ancestor !== root)
58925
- ancestor = path21.dirname(ancestor);
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 success(null, null, roots);
58906
+ return [...seen];
58935
58907
  };
58936
- function captureCandidate(root, scope, environment = []) {
58937
- let checkout;
58908
+ var resolveAutoApproval = (root, dir = configDir()) => {
58909
+ let match;
58938
58910
  try {
58939
- checkout = fs9.realpathSync(root);
58911
+ match = resolveWorkspaceFrom(root, dir);
58940
58912
  } catch {
58941
- return failure("invalid_input", "candidate root does not exist");
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
- 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) => ({
59003
- name,
59004
- value: environment[name] ?? null
59005
- }));
59006
- const namesByEnvironment = supplied.map(({ name }) => name);
59007
- if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
59008
- return failure("invalid_input", "candidate environment names must be unique and non-empty");
59009
- const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
59010
- if (values.some(({ value }) => value === null))
59011
- completeness = "uncertain";
59012
- const headResult = spawnSync3("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
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 sessionFromRef = (value) => {
59041
- if (typeof value !== "object" || value === null || value.kind !== "host")
59042
- return null;
59043
- return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
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 sameSession = (left, right) => {
59046
- const other = sessionFromRef(right);
59047
- return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
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 evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
59050
- function evaluateEvidence(task, candidate) {
59051
- const current = candidate ?? task.candidates.at(-1) ?? null;
59052
- return task.evidence.map((entry) => {
59053
- const evidence = entry.data;
59054
- if (evidence.result === "failed")
59055
- return {
59056
- evidenceId: entry.id,
59057
- status: "failed",
59058
- reason: "failed evidence remains historical"
59059
- };
59060
- if (evidence.kind === "check" || evidence.kind === "review") {
59061
- if (!evidence.beforeCandidateId || !evidence.candidateId)
59062
- return {
59063
- evidenceId: entry.id,
59064
- status: "stale",
59065
- reason: "check or review evidence lacks start and end candidates"
59066
- };
59067
- if (evidence.beforeCandidateId !== evidence.candidateId)
59068
- return {
59069
- evidenceId: entry.id,
59070
- status: "stale",
59071
- reason: "candidate changed during the check or review"
59072
- };
59073
- }
59074
- if (!evidence.candidateId || !current)
59075
- return {
59076
- evidenceId: entry.id,
59077
- status: evidence.result,
59078
- reason: "evidence has no candidate binding"
59079
- };
59080
- const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
59081
- if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
59082
- return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
59083
- if (observed.id === current.id)
59084
- return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
59085
- const scopes = evidenceScopes(task, evidence.requirementIds);
59086
- const changed = changedPaths(observed, current);
59087
- const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
59088
- if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
59089
- return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
59090
- return {
59091
- evidenceId: entry.id,
59092
- status: evidence.result,
59093
- reason: "candidate changed outside evidence scope"
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 resolveBinding = (evidence, pin, currentCandidateId) => {
59105
- const bound = evidence.kind === "check" || evidence.kind === "review";
59106
- const fallback = bound ? pin ?? currentCandidateId : null;
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
- beforeCandidateId: evidence.beforeCandidateId ?? fallback,
59109
- candidateId: evidence.candidateId ?? fallback
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 findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
59113
- 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")));
59114
- 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 }));
59115
- var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
59116
- if (kind !== "passed")
59117
- return false;
59118
- if (dimension === "testing" || dimension === "verification")
59119
- return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
59120
- if (dimension === "review")
59121
- return evidenceKind === "review";
59122
- if (dimension === "investigation" || dimension === "challenge")
59123
- return evidenceKind === "investigation";
59124
- if (dimension === "artifacts" || dimension === "continuity")
59125
- return evidenceKind === "artifact";
59126
- return false;
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
- var expectedKinds = (dimension, ruleId) => {
59129
- if (dimension === "testing")
59130
- return "check";
59131
- if (dimension === "verification")
59132
- return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
59133
- if (dimension === "review")
59134
- return "review";
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
- function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
59144
- const evidence = evaluateEvidence(task, candidate);
59145
- if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
59146
- return task.policy.requirements.map((requirement) => ({
59147
- requirementId: requirement.id,
59148
- status: "unsatisfied",
59149
- evidenceIds: [],
59150
- decisionIds: [],
59151
- reason: "stored policy version is unsupported; reassessment is required"
59152
- }));
59153
- return (task.policy?.requirements ?? []).map((requirement) => {
59154
- const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
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 (requirement.dimension === "delegation") {
59188
- const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
59189
- 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))));
59190
- if (reconciled)
59191
- return {
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
- const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
59200
- if (decisions.length)
59201
- return {
59202
- requirementId: requirement.id,
59203
- status: "satisfied",
59204
- evidenceIds: [],
59205
- decisionIds: decisions.map((decision) => decision.id),
59206
- reason: "an applicable approved or stated decision satisfies the requirement"
59207
- };
59208
- const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
59209
- if (limitations.length)
59210
- return {
59211
- requirementId: requirement.id,
59212
- status: "accepted_limitation",
59213
- evidenceIds: [],
59214
- decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
59215
- reason: "an applicable approved limitation permits the missing evidence"
59216
- };
59217
- const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
59218
- if (!unavailable && related.length > 0 && !passed.length)
59219
- return {
59220
- requirementId: requirement.id,
59221
- status: "unsatisfied",
59222
- evidenceIds: related.map(({ entry }) => entry.id),
59223
- decisionIds: [],
59224
- reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
59225
- };
59226
- return {
59227
- requirementId: requirement.id,
59228
- status: unavailable ? "unavailable" : "unsatisfied",
59229
- evidenceIds: related.map(({ entry }) => entry.id),
59230
- decisionIds: [],
59231
- reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
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
- function evaluateClosure(requestedOutcome, view) {
59236
- if (requestedOutcome !== "stopped" && !view.task.policy)
59237
- return failure("requirements_unsatisfied", "task has not been assessed");
59238
- if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
59239
- return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
59240
- const evaluations = view.requirements;
59241
- const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
59242
- if (requestedOutcome !== "stopped" && openFindings.length)
59243
- return failure("requirements_unsatisfied", "open findings must be resolved before closure");
59244
- if (requestedOutcome !== "stopped") {
59245
- const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
59246
- for (const entry of view.task.findings) {
59247
- if (entry.data.disposition === "fixed") {
59248
- const verified = entry.data.resolution?.evidenceIds.some((id) => {
59249
- const evidence = view.task.evidence.find((item) => item.id === id);
59250
- if (!evidence)
59251
- return false;
59252
- return findingVerificationPasses(entry.data.candidateId, {
59253
- kind: evidence.data.kind,
59254
- candidateId: evidence.data.candidateId,
59255
- status: evidenceById.get(id) ?? "missing"
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
- if (!verified)
59259
- return failure("requirements_unsatisfied", "fixed findings require current verification");
59260
- }
59261
- if (entry.data.disposition === "deferred") {
59262
- const valid = entry.data.resolution?.decisionIds.some((id) => {
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
- const blocking = evaluations.filter((item) => {
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 methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
59329
- var assuranceFor = (id, definition, capabilities) => {
59330
- const keys = new Set([
59331
- id,
59332
- ...definition.dimensions ?? [],
59333
- ...definition.ruleIds ?? []
59334
- ]);
59335
- const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
59336
- if (id === "workit-review" && matches.length === 0)
59337
- return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
59338
- if (matches.some((capability) => capability.assurance === "unavailable"))
59339
- return {
59340
- assurance: "unavailable",
59341
- reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
59342
- };
59343
- if (matches.some((capability) => capability.assurance === "enforced"))
59344
- return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
59345
- return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
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 selectMethods(policy, capabilities) {
59348
- return Object.entries(METHODS).flatMap(([id, definition]) => {
59349
- const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
59350
- if (requirements.length === 0)
59351
- return [];
59352
- const capability = assuranceFor(id, definition, capabilities);
59353
- const reasons = [
59354
- ...new Set(requirements.map((requirement) => requirement.reason)),
59355
- ...capability.reasons
59356
- ];
59357
- if (capability.assurance === "unavailable")
59358
- reasons.unshift("required capability is unavailable");
59359
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
59360
- });
59361
- }
59362
-
59363
- // packages/workit-core/src/core/task-context.ts
59364
- function reconcileResume(view, observations = []) {
59365
- if (observations.length > 0)
59366
- return failure("permission_denied", "worker observations require native host verification");
59367
- const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
59368
- if (!candidate.ok)
59369
- return candidate;
59370
- const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
59371
- const blockers = [...view.task.progress.blockers];
59372
- if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
59373
- blockers.push({
59374
- reason: "worker state requires reconciliation",
59375
- dependentAction: "resume",
59376
- refs: []
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
- return success(null, null, {
59379
- candidate: candidate.data,
59380
- staleEvidenceIds,
59381
- workerUpdates: [],
59382
- blockers,
59383
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
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
- var COMPACT_MAX_BYTES = 4096;
59387
- var COMPACT_MAX_ITEMS = 8;
59388
- var COMPACT_TEXT_BYTES = 128;
59389
- var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
59390
- var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
59391
- var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
59392
- var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
59393
- const separator = match.search(/[:=]/u);
59394
- if (separator >= 0)
59395
- return `${match.slice(0, separator)}=<redacted>`;
59396
- const space = match.search(/\s/u);
59397
- return `${match.slice(0, space)} <redacted>`;
59398
- }).slice(0, limit);
59399
- var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
59400
- var compactReference = (ref) => {
59401
- if (ref.kind === "file")
59402
- return {
59403
- kind: "file",
59404
- path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
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
- if (ref.kind === "record")
59407
- return {
59408
- kind: "record",
59409
- collection: ref.collection,
59410
- id: ref.id
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
- return { kind: ref.kind };
59413
- };
59414
- function compactTaskContext(view) {
59415
- 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) => ({
59416
- id: entry.id,
59417
- purpose: entry.data.purpose,
59418
- status: entry.data.response,
59419
- digest: entry.data.digest,
59420
- references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
59421
- }));
59422
- const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
59423
- id: method.id,
59424
- assurance: method.assurance,
59425
- reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
59426
- })) : [];
59427
- const gaps = Array.from(new Set([
59428
- ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
59429
- ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
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 exportDigest = (bundle) => sha256({
59458
- schemaVersion: bundle.schemaVersion,
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/git.ts
59469
- import { execFileSync as execFileSync2 } from "node:child_process";
59470
- var run = (cwd, args) => {
59471
- try {
59472
- const stdout = execFileSync2("git", args, {
59473
- cwd,
59474
- encoding: "utf8",
59475
- stdio: ["pipe", "pipe", "pipe"]
59476
- });
59477
- return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
59478
- } catch (error) {
59479
- const e = error;
59480
- return {
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 gitContext = (workspaceRoot, paths = []) => {
59488
- const cwd = workspaceRoot;
59489
- let failure;
59490
- const runHere = (args) => {
59491
- const result = run(cwd, args);
59492
- if (!failure && result.exitCode !== 0) {
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
- workspace_root: cwd,
59523
- branch,
59524
- status_short,
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
- // packages/workit-core/src/core/vcs-config.ts
59537
- import fs10 from "node:fs";
59538
- import path22 from "node:path";
59539
- import { spawnSync as spawnSync4 } from "node:child_process";
59540
- var TOKEN_PLACEHOLDER3 = "YOUR_TOKEN_HERE";
59541
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path22.join(configDir(), "vcs.json");
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 readVcsConfig = () => {
59555
- const file = vcsConfigPath();
59556
- let raw;
59557
- try {
59558
- raw = fs10.readFileSync(file, "utf8");
59559
- } catch {
59560
- return { status: "missing", path: file, config: {} };
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
- let parsed;
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
- parsed = JSON.parse(raw);
59687
+ names = fs10.readdirSync(directory);
59565
59688
  } catch {
59566
- return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
59689
+ return true;
59567
59690
  }
59568
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
59569
- return { status: "valid", path: file, config: parsed };
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 { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
59708
+ return uncertain;
59572
59709
  };
59573
- function vcsConfig(mode, cwd) {
59574
- const ws = resolveWorkspace(vcsCwd(cwd));
59575
- const wsVcs = ws?.vcs ?? {};
59576
- const wsYt = ws?.youtrack ?? {};
59577
- const wsIssues = ws?.issues ?? {};
59578
- const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
59579
- const root = vcsCwd(cwd);
59580
- const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
59581
- const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
59582
- const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
59583
- const wp = ws?.branchPolicy ?? {};
59584
- const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
59585
- const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
59586
- const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
59587
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
59588
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
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
- if (cfgStatus === "missing") {
59613
- return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
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
- if (provider === null) {
59616
- return {
59617
- ok: false,
59618
- error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
59619
- configPath: cfgPath
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 prov = cfg[provider] ?? {};
59623
- const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
59624
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path22.join(configDir(), `${provider}.token`));
59625
- const tokenPath = path22.resolve(tokenFile);
59626
- let tokenOk = false;
59627
- if (fs10.existsSync(tokenPath)) {
59628
- const token = fs10.readFileSync(tokenPath, "utf8").trim();
59629
- const placeholder = !token || token === TOKEN_PLACEHOLDER3 || token.startsWith(TOKEN_PLACEHOLDER3);
59630
- tokenOk = !placeholder;
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 out = {
59633
- ok: true,
59634
- configPath: path22.resolve(cfgPath),
59635
- provider,
59636
- defaultTargetBranch: defaultTarget,
59637
- pr: cfg.pr ?? {},
59638
- tokenPath,
59639
- tokenPresent: fs10.existsSync(tokenPath),
59640
- tokenReady: tokenOk,
59641
- workspace_name: ws?.name ?? null,
59642
- link_issues: linkIssues,
59643
- youtrack_base_url: youtrackBaseUrl,
59644
- issues_provider: issuesProvider,
59645
- link_on_pr: linkOnPr
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
- if (provider === "gitlab") {
59648
- out.gitlab = {
59649
- host: prov.host ?? "gitlab.com",
59650
- apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
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
- if (provider === "gitlab") {
59673
- const remote = spawnSync4("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
59674
- if (remote.status !== 0)
59675
- return { ok: false, error: "no origin remote" };
59676
- const url = (remote.stdout ?? "").trim();
59677
- const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
59678
- if (!m)
59679
- return { ok: false, error: "not a gitlab.com origin" };
59680
- const project = m[1];
59681
- const env = { ...process.env, GITLAB_TOKEN: token };
59682
- const run = (args) => spawnSync4("glab", ["api", ...args], { cwd, encoding: "utf8", env });
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
- ok: true,
59711
- provider,
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 originBaseReady = (cwd, base) => {
59735
- const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
59736
- try {
59737
- try {
59738
- run(["fetch", "origin", base, "--prune"]);
59739
- } catch {
59740
- run(["fetch", "origin", "--prune"]);
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
- try {
59743
- execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
59744
- cwd,
59745
- stdio: "pipe"
59746
- });
59747
- } catch {
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
- ok: false,
59750
- error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`
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 fastForwardBase = (cwd, base) => {
59762
- const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
59763
- try {
59764
- let hasLocalBase = true;
59765
- try {
59766
- execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
59767
- cwd,
59768
- stdio: "pipe"
59769
- });
59770
- } catch {
59771
- hasLocalBase = false;
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
- ok: false,
59785
- error: error instanceof Error ? error.message : "ensure-base-branch failed"
60133
+ assurance: "unavailable",
60134
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
59786
60135
  };
59787
- }
59788
- };
59789
- var ensureBaseBranch = (cwd, base) => {
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
- var classifyBranchDirt = (workspaceRoot) => {
59799
- let raw;
59800
- try {
59801
- raw = execFileSync3("git", ["status", "--porcelain=v1"], {
59802
- cwd: path23.resolve(workspaceRoot),
59803
- encoding: "utf8",
59804
- stdio: ["pipe", "pipe", "pipe"]
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
- } catch {
59807
- return "clean";
59808
- }
59809
- let dirty = false;
59810
- for (const line of raw.split(`
59811
- `)) {
59812
- if (!line.trim())
59813
- continue;
59814
- dirty = true;
59815
- const code = line.slice(0, 2);
59816
- if (code === "??")
59817
- continue;
59818
- let entry = line.slice(3);
59819
- const arrow = entry.indexOf(" -> ");
59820
- if (arrow >= 0)
59821
- entry = entry.slice(arrow + 4);
59822
- const unquoted = entry.startsWith('"') && entry.endsWith('"') ? entry.slice(1, -1) : entry;
59823
- if (unquoted !== "docs" && !unquoted.startsWith("docs/"))
59824
- return "stash-required";
59825
- }
59826
- return dirty ? "carry" : "clean";
59827
- };
59828
- var branchSetup = ({
59829
- action,
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
- error: "target_branch (the working branch to create or switch to) is required for setup",
59886
- phase: "preflight"
60196
+ kind: "file",
60197
+ path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
59887
60198
  };
59888
- if (isProtected(cwd, target))
59889
- return { error: `protected branch ${target}`, phase: "preflight" };
59890
- if (!allowedBranch(cwd, target))
60199
+ if (ref.kind === "record")
59891
60200
  return {
59892
- error: `target branch ${target} is not allowed by the branch policy`,
59893
- phase: "preflight"
60201
+ kind: "record",
60202
+ collection: ref.collection,
60203
+ id: ref.id
59894
60204
  };
59895
- let base;
59896
- let targetExists = true;
59897
- try {
59898
- exec(["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]);
59899
- } catch {
59900
- targetExists = false;
59901
- }
59902
- if (!targetExists) {
59903
- const baseResolved = baseBranch(cwd);
59904
- if ("error" in baseResolved)
59905
- return { error: baseResolved.error, phase: "preflight" };
59906
- base = baseResolved.base;
59907
- }
59908
- journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
59909
- let stash_ref;
59910
- const failAfterStash = (message, phase = "post_mutation") => {
59911
- let suffix = "";
59912
- if (stash_ref) {
59913
- journal(`pre-pop: ${stash_ref}`);
59914
- try {
59915
- exec(["stash", "pop", stash_ref]);
59916
- stash_ref = undefined;
59917
- journal("pop: ok");
59918
- } catch {
59919
- journal("pop: failed");
59920
- suffix = " (changes preserved in stash)";
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
- if (current !== target) {
59926
- const dirty = Boolean(gitContext(cwd).status_short.trim());
59927
- if (classifyBranchDirt(cwd) === "stash-required" && stash !== "yes") {
59928
- return {
59929
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
59930
- phase: "preflight"
59931
- };
59932
- }
59933
- let validatedBase;
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
- try {
59979
- const manifest = readManifest();
59980
- manifest.branch = target;
59981
- manifest.previous_branch = current;
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
- action: "setup",
60002
- ok: true,
60003
- branch: target,
60004
- previous_branch: current,
60005
- stash_ref: stash_ref ?? null,
60006
- manifest: manifestPath
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
- recordDecision(request, nativeObservation, nativeRequired = false) {
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 native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
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 readFileSync17, readdirSync as readdirSync9 } from "node:fs";
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(readFileSync17(pkgPath, "utf8"));
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 = readFileSync17(file, "utf8").split(`
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(readFileSync17(pkgPath, "utf8"));
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(readFileSync18(raw.slice(1))) : raw;
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
- if (!tty || !argv.includes("--confirm")) {
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);