@brainervirus/workit-cursor 1.0.12 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cursor-session-start.js +421 -84
- package/dist/mcp-server.js +1701 -1517
- package/dist/workit-hook.js +416 -54
- package/package.json +3 -3
package/dist/mcp-server.js
CHANGED
|
@@ -6625,7 +6625,7 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
|
6625
6625
|
import path15 from "node:path";
|
|
6626
6626
|
|
|
6627
6627
|
// packages/workit-mcp/src/server.ts
|
|
6628
|
-
import { existsSync as existsSync7, readFileSync as
|
|
6628
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync7 } from "node:fs";
|
|
6629
6629
|
import path14 from "node:path";
|
|
6630
6630
|
|
|
6631
6631
|
// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/core.js
|
|
@@ -20052,7 +20052,13 @@ var refSchema = discriminatedUnion2("kind", [
|
|
|
20052
20052
|
id
|
|
20053
20053
|
}).strict(),
|
|
20054
20054
|
object4({ kind: literal3("host"), host: hostSchema, handle: nonEmpty }).strict(),
|
|
20055
|
-
object4({ kind: literal3("external"), url: text.url() }).strict()
|
|
20055
|
+
object4({ kind: literal3("external"), url: text.url() }).strict(),
|
|
20056
|
+
object4({
|
|
20057
|
+
kind: literal3("standing"),
|
|
20058
|
+
workspace: nonEmpty,
|
|
20059
|
+
class: nonEmpty,
|
|
20060
|
+
configDigest: nullableDigest
|
|
20061
|
+
}).strict()
|
|
20056
20062
|
]);
|
|
20057
20063
|
var rewriteUnknown = (value, map) => {
|
|
20058
20064
|
if (Array.isArray(value)) {
|
|
@@ -20357,7 +20363,8 @@ var decisionSchema = object4({
|
|
|
20357
20363
|
approvedContent: text,
|
|
20358
20364
|
displayed: text.optional(),
|
|
20359
20365
|
contentRefs: array3(refSchema),
|
|
20360
|
-
statedChoice: object4({ ref: text, text }).strict().optional()
|
|
20366
|
+
statedChoice: object4({ ref: text, text }).strict().optional(),
|
|
20367
|
+
standing: object4({ workspace: nonEmpty, class: nonEmpty }).strict().optional()
|
|
20361
20368
|
}).strict(),
|
|
20362
20369
|
digest,
|
|
20363
20370
|
response: _enum2(["approved", "rejected", "stated"]),
|
|
@@ -22433,1622 +22440,1784 @@ function assertProductWriteAllowed(input) {
|
|
|
22433
22440
|
}
|
|
22434
22441
|
|
|
22435
22442
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
22443
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
22444
|
+
import * as fs7 from "node:fs";
|
|
22445
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
22446
|
+
import path9 from "node:path";
|
|
22447
|
+
|
|
22448
|
+
// packages/workit-core/src/core/authority.ts
|
|
22436
22449
|
import { createHash as createHash4 } from "node:crypto";
|
|
22437
22450
|
import * as fs6 from "node:fs";
|
|
22438
|
-
import
|
|
22439
|
-
import path6 from "node:path";
|
|
22451
|
+
import path8 from "node:path";
|
|
22440
22452
|
|
|
22441
|
-
// packages/workit-core/src/core/
|
|
22453
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
22442
22454
|
import { createHash as createHash3 } from "node:crypto";
|
|
22443
|
-
import
|
|
22455
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
22456
|
+
|
|
22457
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
22458
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
22459
|
+
import path6 from "node:path";
|
|
22460
|
+
|
|
22461
|
+
// packages/workit-core/src/core/config.ts
|
|
22462
|
+
import {
|
|
22463
|
+
copyFileSync,
|
|
22464
|
+
cpSync,
|
|
22465
|
+
existsSync as existsSync3,
|
|
22466
|
+
mkdirSync as mkdirSync2,
|
|
22467
|
+
readFileSync as readFileSync2,
|
|
22468
|
+
readdirSync as readdirSync2,
|
|
22469
|
+
writeFileSync as writeFileSync2
|
|
22470
|
+
} from "node:fs";
|
|
22471
|
+
import os from "node:os";
|
|
22444
22472
|
import path5 from "node:path";
|
|
22445
|
-
|
|
22446
|
-
|
|
22447
|
-
|
|
22448
|
-
|
|
22449
|
-
|
|
22473
|
+
|
|
22474
|
+
// packages/workit-core/src/core/boundary.ts
|
|
22475
|
+
import { statSync } from "node:fs";
|
|
22476
|
+
var EVENT = {
|
|
22477
|
+
initialization: "initialization",
|
|
22478
|
+
provenance: "provenance",
|
|
22479
|
+
assets: "assets",
|
|
22480
|
+
configurationSource: "configuration_source",
|
|
22481
|
+
hooks: "hooks",
|
|
22482
|
+
mcpConnection: "mcp_connection",
|
|
22483
|
+
migration: "migration",
|
|
22484
|
+
installSteps: "install_steps",
|
|
22485
|
+
uncaughtFailure: "uncaught_failure",
|
|
22486
|
+
toolsFailed: "tools_failed",
|
|
22487
|
+
doctor: "doctor"
|
|
22450
22488
|
};
|
|
22451
|
-
var
|
|
22452
|
-
|
|
22453
|
-
|
|
22454
|
-
|
|
22455
|
-
|
|
22456
|
-
|
|
22457
|
-
|
|
22458
|
-
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
|
|
22463
|
-
|
|
22464
|
-
|
|
22465
|
-
|
|
22466
|
-
|
|
22467
|
-
|
|
22468
|
-
|
|
22469
|
-
|
|
22470
|
-
|
|
22471
|
-
|
|
22489
|
+
var errorDetail = (err) => {
|
|
22490
|
+
if (err instanceof Error) {
|
|
22491
|
+
return { error_name: err.name, error: err.message };
|
|
22492
|
+
}
|
|
22493
|
+
return { error_name: "unknown", error: String(err) };
|
|
22494
|
+
};
|
|
22495
|
+
var markSourcesLoaded = (files, loadedAt = Date.now()) => ({
|
|
22496
|
+
loadedAt,
|
|
22497
|
+
files: [...files]
|
|
22498
|
+
});
|
|
22499
|
+
var changedSourcesSinceLoad = (marker) => {
|
|
22500
|
+
const changed = [];
|
|
22501
|
+
for (const file of marker.files) {
|
|
22502
|
+
try {
|
|
22503
|
+
if (statSync(file).mtimeMs > marker.loadedAt)
|
|
22504
|
+
changed.push(file);
|
|
22505
|
+
} catch {}
|
|
22506
|
+
}
|
|
22507
|
+
return changed;
|
|
22508
|
+
};
|
|
22509
|
+
|
|
22510
|
+
// packages/workit-core/src/core/config.ts
|
|
22511
|
+
var diagnosticLogger;
|
|
22512
|
+
var PRESETS = {
|
|
22513
|
+
gitflow: {
|
|
22514
|
+
allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
|
|
22515
|
+
protected: ["main", "develop", "master", "prod", "production"]
|
|
22516
|
+
},
|
|
22517
|
+
"github-flow": { allowed: ["*"], protected: ["main"] },
|
|
22518
|
+
"trunk-based": { allowed: ["*"], protected: ["main"] },
|
|
22519
|
+
custom: { allowed: [], protected: [] }
|
|
22520
|
+
};
|
|
22521
|
+
var mergePreset = (preset, input = {}, current = {
|
|
22522
|
+
branchPolicy: { preset, allowed: [], protected: [] }
|
|
22523
|
+
}) => {
|
|
22524
|
+
const defs = PRESETS[preset];
|
|
22525
|
+
return {
|
|
22526
|
+
preset,
|
|
22527
|
+
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
22528
|
+
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
22529
|
+
};
|
|
22530
|
+
};
|
|
22531
|
+
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workit");
|
|
22532
|
+
var migratedDir = null;
|
|
22533
|
+
var migrationFailed = false;
|
|
22534
|
+
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
22535
|
+
if (migratedDir === dir)
|
|
22536
|
+
return dir;
|
|
22537
|
+
if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
|
|
22538
|
+
migratedDir = dir;
|
|
22539
|
+
return dir;
|
|
22540
|
+
}
|
|
22541
|
+
const legacy = path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
22542
|
+
if (!existsSync3(legacy)) {
|
|
22543
|
+
migratedDir = dir;
|
|
22544
|
+
return dir;
|
|
22545
|
+
}
|
|
22546
|
+
if (!migrationFailed && existsSync3(dir)) {
|
|
22547
|
+
migratedDir = dir;
|
|
22548
|
+
return dir;
|
|
22549
|
+
}
|
|
22550
|
+
migrationFailed = false;
|
|
22551
|
+
mkdirSync2(dir, { recursive: true });
|
|
22552
|
+
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
22553
|
+
for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
|
|
22554
|
+
const src = path5.join(legacy, entry.name);
|
|
22555
|
+
const dest = path5.join(dir, entry.name);
|
|
22556
|
+
if (existsSync3(dest))
|
|
22472
22557
|
continue;
|
|
22558
|
+
try {
|
|
22559
|
+
if (entry.isDirectory())
|
|
22560
|
+
cpSync(src, dest, { recursive: true });
|
|
22561
|
+
else if (entry.isFile())
|
|
22562
|
+
copyFileSync(src, dest);
|
|
22563
|
+
} catch (err) {
|
|
22564
|
+
migrationFailed = true;
|
|
22565
|
+
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
22566
|
+
console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
|
|
22473
22567
|
}
|
|
22474
|
-
return null;
|
|
22475
22568
|
}
|
|
22476
|
-
|
|
22477
|
-
|
|
22569
|
+
if (!migrationFailed)
|
|
22570
|
+
migratedDir = dir;
|
|
22571
|
+
return dir;
|
|
22572
|
+
};
|
|
22573
|
+
var configDir = () => ensureConfigDir();
|
|
22574
|
+
var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
22575
|
+
var DEFAULTS = {
|
|
22576
|
+
locale: "en",
|
|
22577
|
+
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
22578
|
+
timezone: "America/Santiago",
|
|
22579
|
+
branchPolicy: {
|
|
22580
|
+
preset: "gitflow",
|
|
22581
|
+
allowed: [...PRESETS.gitflow.allowed],
|
|
22582
|
+
protected: [...PRESETS.gitflow.protected]
|
|
22583
|
+
},
|
|
22584
|
+
commitPolicy: { preset: "conventional" }
|
|
22585
|
+
};
|
|
22586
|
+
var readSafe = (p) => {
|
|
22587
|
+
try {
|
|
22588
|
+
return readFileSync2(p, "utf8");
|
|
22589
|
+
} catch {
|
|
22478
22590
|
return null;
|
|
22479
|
-
|
|
22591
|
+
}
|
|
22480
22592
|
};
|
|
22481
|
-
var
|
|
22482
|
-
var
|
|
22483
|
-
|
|
22593
|
+
var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22594
|
+
var parseConfigResult = (raw, file) => {
|
|
22595
|
+
if (raw === null)
|
|
22596
|
+
return { status: "missing", path: file };
|
|
22597
|
+
let parsed;
|
|
22484
22598
|
try {
|
|
22485
|
-
|
|
22599
|
+
parsed = JSON.parse(raw);
|
|
22486
22600
|
} catch {
|
|
22487
|
-
return
|
|
22601
|
+
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
22488
22602
|
}
|
|
22489
|
-
if (
|
|
22490
|
-
return
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
const
|
|
22495
|
-
|
|
22603
|
+
if (!isConfigObject(parsed)) {
|
|
22604
|
+
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
22605
|
+
}
|
|
22606
|
+
const input = parsed;
|
|
22607
|
+
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
22608
|
+
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
22609
|
+
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
22610
|
+
return {
|
|
22611
|
+
status: "valid",
|
|
22612
|
+
path: file,
|
|
22613
|
+
config: {
|
|
22614
|
+
locale,
|
|
22615
|
+
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
22616
|
+
timezone: input.timezone ?? DEFAULTS.timezone,
|
|
22617
|
+
branchPolicy: mergePreset(preset, {
|
|
22618
|
+
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
22619
|
+
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
22620
|
+
}, DEFAULTS),
|
|
22621
|
+
commitPolicy: {
|
|
22622
|
+
preset: commitPreset,
|
|
22623
|
+
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
22624
|
+
}
|
|
22625
|
+
}
|
|
22626
|
+
};
|
|
22496
22627
|
};
|
|
22497
|
-
var
|
|
22498
|
-
const
|
|
22499
|
-
return
|
|
22628
|
+
var readConfigTyped = (dir) => {
|
|
22629
|
+
const file = path5.join(dir ?? configDir(), "config.json");
|
|
22630
|
+
return parseConfigResult(readSafe(file), file);
|
|
22500
22631
|
};
|
|
22501
|
-
var
|
|
22502
|
-
|
|
22503
|
-
if (
|
|
22504
|
-
|
|
22505
|
-
|
|
22506
|
-
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);
|
|
22632
|
+
var readConfig = () => {
|
|
22633
|
+
const result = readConfigTyped();
|
|
22634
|
+
if (result.status === "malformed")
|
|
22635
|
+
throw new Error(result.error);
|
|
22636
|
+
return result.config ?? DEFAULTS;
|
|
22507
22637
|
};
|
|
22508
|
-
var
|
|
22509
|
-
|
|
22510
|
-
|
|
22511
|
-
const
|
|
22512
|
-
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
|
|
22520
|
-
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
}, binding, input.caller));
|
|
22638
|
+
var resolveBranchPolicy = (config, workspace) => {
|
|
22639
|
+
const wp = workspace?.branchPolicy ?? {};
|
|
22640
|
+
const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
|
|
22641
|
+
const merged = mergePreset(preset, {
|
|
22642
|
+
allowed: wp.allowed,
|
|
22643
|
+
protectedNames: wp.protected
|
|
22644
|
+
}, config);
|
|
22645
|
+
const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
|
|
22646
|
+
return {
|
|
22647
|
+
preset,
|
|
22648
|
+
allowed,
|
|
22649
|
+
protected: new Set(merged.protected.map((p) => p.toLowerCase())),
|
|
22650
|
+
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
22651
|
+
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
22652
|
+
};
|
|
22524
22653
|
};
|
|
22525
|
-
var
|
|
22526
|
-
|
|
22527
|
-
|
|
22528
|
-
|
|
22529
|
-
|
|
22530
|
-
|
|
22531
|
-
|
|
22532
|
-
|
|
22533
|
-
|
|
22534
|
-
|
|
22535
|
-
|
|
22536
|
-
|
|
22537
|
-
|
|
22538
|
-
|
|
22539
|
-
|
|
22540
|
-
|
|
22541
|
-
|
|
22542
|
-
|
|
22543
|
-
|
|
22544
|
-
|
|
22545
|
-
|
|
22546
|
-
|
|
22547
|
-
|
|
22548
|
-
|
|
22549
|
-
|
|
22550
|
-
|
|
22551
|
-
|
|
22552
|
-
|
|
22553
|
-
|
|
22554
|
-
|
|
22555
|
-
|
|
22556
|
-
|
|
22557
|
-
|
|
22558
|
-
workspaceRevision: input.expected.workspaceRevision,
|
|
22559
|
-
outcome: input.expected.outcome,
|
|
22560
|
-
evidenceDigest: input.expected.evidenceDigest,
|
|
22561
|
-
step: input.expected.step,
|
|
22562
|
-
purpose: input.expected.decision.purpose,
|
|
22563
|
-
response: input.expected.decision.response,
|
|
22564
|
-
binding: input.expected.decision.binding,
|
|
22565
|
-
digest: input.expected.decision.digest,
|
|
22566
|
-
requirementIds: [...input.expected.decision.requirementIds]
|
|
22567
|
-
}, binding, input.caller));
|
|
22568
|
-
};
|
|
22569
|
-
var takeAuthority = (authority, binding, caller) => {
|
|
22570
|
-
const record = verifiedAuthorities.get(authority);
|
|
22571
|
-
verifiedAuthorities.delete(authority);
|
|
22572
|
-
if (!record)
|
|
22573
|
-
return null;
|
|
22574
|
-
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
22575
|
-
return null;
|
|
22576
|
-
return record;
|
|
22654
|
+
var COMMIT_PRESETS = [
|
|
22655
|
+
"conventional",
|
|
22656
|
+
"gitmoji",
|
|
22657
|
+
"ticket-prefix",
|
|
22658
|
+
"freeform",
|
|
22659
|
+
"custom",
|
|
22660
|
+
"auto"
|
|
22661
|
+
];
|
|
22662
|
+
|
|
22663
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
22664
|
+
var readWorkspacesResult = (dir = configDir()) => {
|
|
22665
|
+
const file = path6.join(dir, "workspaces.json");
|
|
22666
|
+
let raw;
|
|
22667
|
+
try {
|
|
22668
|
+
raw = readFileSync3(file, "utf8");
|
|
22669
|
+
} catch {
|
|
22670
|
+
return { status: "missing", path: file, entries: [] };
|
|
22671
|
+
}
|
|
22672
|
+
let parsed;
|
|
22673
|
+
try {
|
|
22674
|
+
parsed = JSON.parse(raw);
|
|
22675
|
+
} catch {
|
|
22676
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
|
|
22677
|
+
}
|
|
22678
|
+
if (!isConfigObject(parsed)) {
|
|
22679
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
22680
|
+
}
|
|
22681
|
+
const list = parsed.workspaces;
|
|
22682
|
+
return {
|
|
22683
|
+
status: "valid",
|
|
22684
|
+
path: file,
|
|
22685
|
+
entries: Array.isArray(list) ? list : []
|
|
22686
|
+
};
|
|
22577
22687
|
};
|
|
22578
|
-
var
|
|
22579
|
-
|
|
22580
|
-
|
|
22581
|
-
|
|
22688
|
+
var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
|
|
22689
|
+
var globToRegExp = (glob) => {
|
|
22690
|
+
let out = "";
|
|
22691
|
+
for (let i = 0;i < glob.length; i++) {
|
|
22692
|
+
const c = glob[i];
|
|
22693
|
+
if (c === "*") {
|
|
22694
|
+
if (glob[i + 1] === "*") {
|
|
22695
|
+
if (glob[i + 2] === "/") {
|
|
22696
|
+
if (out === "")
|
|
22697
|
+
out += "/?";
|
|
22698
|
+
out += "(?:[^/]+/)*";
|
|
22699
|
+
i += 2;
|
|
22700
|
+
} else {
|
|
22701
|
+
if (i + 2 >= glob.length) {
|
|
22702
|
+
if (out.endsWith("/")) {
|
|
22703
|
+
out = out.slice(0, -1);
|
|
22704
|
+
out += "(?:/.*)?";
|
|
22705
|
+
} else {
|
|
22706
|
+
out += ".*";
|
|
22707
|
+
}
|
|
22708
|
+
} else {
|
|
22709
|
+
out += ".*";
|
|
22710
|
+
}
|
|
22711
|
+
i++;
|
|
22712
|
+
}
|
|
22713
|
+
} else {
|
|
22714
|
+
out += "[^/]*";
|
|
22715
|
+
}
|
|
22716
|
+
} else {
|
|
22717
|
+
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
22718
|
+
}
|
|
22719
|
+
}
|
|
22720
|
+
return new RegExp(`^${out}$`);
|
|
22582
22721
|
};
|
|
22583
|
-
var
|
|
22584
|
-
var
|
|
22585
|
-
|
|
22586
|
-
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
22587
|
-
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
22588
|
-
return [];
|
|
22589
|
-
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);
|
|
22590
|
-
}
|
|
22591
|
-
var digestBytes2 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
|
|
22592
|
-
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
22593
|
-
let root;
|
|
22722
|
+
var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
|
|
22723
|
+
var realpathOf = (p) => {
|
|
22724
|
+
const real = realpathSync2.native ?? realpathSync2;
|
|
22594
22725
|
try {
|
|
22595
|
-
|
|
22726
|
+
return real(p);
|
|
22596
22727
|
} catch {
|
|
22597
|
-
return
|
|
22728
|
+
return p;
|
|
22598
22729
|
}
|
|
22599
|
-
|
|
22600
|
-
|
|
22730
|
+
};
|
|
22731
|
+
var canonicalGlob = (glob) => {
|
|
22732
|
+
const m = /[*?[\]{]/.exec(glob);
|
|
22733
|
+
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
22734
|
+
const rest = m ? glob.slice(m.index) : "";
|
|
22735
|
+
if (!prefix)
|
|
22736
|
+
return glob;
|
|
22737
|
+
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
22738
|
+
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
22739
|
+
};
|
|
22740
|
+
var resolveWorkspaceFrom = (cwd, dir) => {
|
|
22741
|
+
const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
|
|
22742
|
+
for (const entry of loadWorkspacesFrom(dir)) {
|
|
22743
|
+
if (!entry || typeof entry !== "object")
|
|
22601
22744
|
continue;
|
|
22602
|
-
|
|
22603
|
-
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22608
|
-
|
|
22609
|
-
|
|
22610
|
-
|
|
22611
|
-
|
|
22612
|
-
const relative = path5.relative(root, target);
|
|
22613
|
-
let current = root;
|
|
22614
|
-
for (const segment of relative.split(path5.sep)) {
|
|
22615
|
-
if (!segment)
|
|
22616
|
-
continue;
|
|
22617
|
-
current = path5.join(current, segment);
|
|
22618
|
-
const stat = fs5.lstatSync(current);
|
|
22619
|
-
const real = fs5.realpathSync(current);
|
|
22620
|
-
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
22621
|
-
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
22622
|
-
fields: [
|
|
22623
|
-
{
|
|
22624
|
-
path: "contentRefs.path",
|
|
22625
|
-
reason: `${reference.path} resolves outside the checkout`
|
|
22626
|
-
}
|
|
22627
|
-
]
|
|
22628
|
-
});
|
|
22629
|
-
if (stat.isSymbolicLink())
|
|
22630
|
-
return failure("invalid_input", "document reference uses a symlink");
|
|
22631
|
-
if (current === target && !stat.isFile())
|
|
22632
|
-
return failure("invalid_input", "document reference is not a regular file");
|
|
22633
|
-
if (current !== target && !stat.isDirectory())
|
|
22634
|
-
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
22635
|
-
}
|
|
22636
|
-
if (digestBytes2(fs5.readFileSync(target)) !== reference.digest)
|
|
22637
|
-
return failure("permission_denied", "approved document bytes have changed");
|
|
22638
|
-
} catch {
|
|
22639
|
-
return failure("invalid_input", "approved document is unavailable");
|
|
22745
|
+
const ws = entry;
|
|
22746
|
+
if (typeof ws.glob !== "string" || !ws.glob)
|
|
22747
|
+
continue;
|
|
22748
|
+
const glob = ws.glob.replaceAll("\\", "/");
|
|
22749
|
+
const canonical = canonicalGlob(glob);
|
|
22750
|
+
for (const target of targets) {
|
|
22751
|
+
if (matchWorkspace(glob, target))
|
|
22752
|
+
return ws;
|
|
22753
|
+
if (canonical !== glob && matchWorkspace(canonical, target))
|
|
22754
|
+
return ws;
|
|
22640
22755
|
}
|
|
22641
22756
|
}
|
|
22642
|
-
return
|
|
22757
|
+
return null;
|
|
22643
22758
|
};
|
|
22644
|
-
var
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
22654
|
-
|
|
22655
|
-
|
|
22656
|
-
|
|
22657
|
-
|
|
22658
|
-
|
|
22659
|
-
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
if (!authorityDecisionMatches(authority, decision))
|
|
22668
|
-
return failure("permission_denied", "native reservation authority does not match decision");
|
|
22669
|
-
if (decision.response !== "approved")
|
|
22670
|
-
return failure("permission_denied", "decision was rejected");
|
|
22671
|
-
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
22672
|
-
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
22673
|
-
if (decision.revoked)
|
|
22674
|
-
return failure("permission_denied", "decision is revoked");
|
|
22675
|
-
if (decision.digest !== decisionDigest(decision))
|
|
22676
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
22677
|
-
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
22678
|
-
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
22679
|
-
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
22680
|
-
return failure("permission_denied", "action binding does not match the approved decision");
|
|
22681
|
-
const content = verifyContentRefs(store, decision.binding);
|
|
22682
|
-
if (!content.ok)
|
|
22683
|
-
return content;
|
|
22684
|
-
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
22685
|
-
if (requestedSteps === null)
|
|
22686
|
-
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
22687
|
-
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
22688
|
-
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
22689
|
-
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
22690
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
22691
|
-
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
22692
|
-
return failure("permission_denied", "bounded action scope changed");
|
|
22693
|
-
return success(null, null, { entry, workflow });
|
|
22759
|
+
var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
|
|
22760
|
+
|
|
22761
|
+
// packages/workit-core/src/core/branch.ts
|
|
22762
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
22763
|
+
|
|
22764
|
+
// packages/workit-core/src/core/git.ts
|
|
22765
|
+
import { execFileSync } from "node:child_process";
|
|
22766
|
+
var run = (cwd, args) => {
|
|
22767
|
+
try {
|
|
22768
|
+
const stdout = execFileSync("git", args, {
|
|
22769
|
+
cwd,
|
|
22770
|
+
encoding: "utf8",
|
|
22771
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
22772
|
+
});
|
|
22773
|
+
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
22774
|
+
} catch (error) {
|
|
22775
|
+
const e = error;
|
|
22776
|
+
return {
|
|
22777
|
+
stdout: (e.stdout ?? "").trimEnd(),
|
|
22778
|
+
stderr: (e.stderr ?? "").trimEnd(),
|
|
22779
|
+
exitCode: e.status ?? 1
|
|
22780
|
+
};
|
|
22781
|
+
}
|
|
22694
22782
|
};
|
|
22695
|
-
|
|
22696
|
-
|
|
22697
|
-
|
|
22698
|
-
const
|
|
22699
|
-
|
|
22700
|
-
|
|
22701
|
-
|
|
22702
|
-
}, input.authorityCaller);
|
|
22703
|
-
if (!authority)
|
|
22704
|
-
return failure("permission_denied", "native reservation authority is not verified");
|
|
22705
|
-
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
22706
|
-
const workspace = input.store.readWorkspace();
|
|
22707
|
-
if (!workspace.ok)
|
|
22708
|
-
return workspace;
|
|
22709
|
-
if (!workspace.data)
|
|
22710
|
-
return failure("not_found", "workspace not found");
|
|
22711
|
-
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
22712
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
22713
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
22714
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
22715
|
-
});
|
|
22716
|
-
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
22717
|
-
if (!valid.ok)
|
|
22718
|
-
return valid;
|
|
22719
|
-
const { entry, workflow } = valid.data;
|
|
22720
|
-
const existing = entry.data.consumption;
|
|
22721
|
-
if (existing?.state === "uncertain")
|
|
22722
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
22723
|
-
operation: "reserve_action",
|
|
22724
|
-
outcome: "unknown"
|
|
22725
|
-
});
|
|
22726
|
-
if (existing?.state === "consumed")
|
|
22727
|
-
return failure("permission_denied", "action approval is consumed");
|
|
22728
|
-
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
22729
|
-
return failure("permission_denied", "action approval is already reserved");
|
|
22730
|
-
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
22731
|
-
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
22732
|
-
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
22733
|
-
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
22734
|
-
return failure("permission_denied", "action step was already completed");
|
|
22735
|
-
const nextDecision = {
|
|
22736
|
-
...entry.data,
|
|
22737
|
-
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
22738
|
-
};
|
|
22739
|
-
const actionProgress = workflow.steps.length ? [
|
|
22740
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
22741
|
-
{
|
|
22742
|
-
decisionId: input.decisionId,
|
|
22743
|
-
steps: [...workflow.steps],
|
|
22744
|
-
completedSteps: [...workflow.completed]
|
|
22745
|
-
}
|
|
22746
|
-
] : current.actionProgress ?? [];
|
|
22747
|
-
return success(mutation.revision, null, {
|
|
22748
|
-
...current,
|
|
22749
|
-
actionProgress,
|
|
22750
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
22751
|
-
});
|
|
22752
|
-
}, input.native?.now);
|
|
22753
|
-
if (!changed.ok)
|
|
22754
|
-
return changed;
|
|
22755
|
-
const workspace = input.store.readWorkspace();
|
|
22756
|
-
if (!workspace.ok || !workspace.data)
|
|
22757
|
-
return failure("recovery_required", "workspace disappeared after reservation");
|
|
22758
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
22759
|
-
if (!entry)
|
|
22760
|
-
return failure("recovery_required", "reserved decision disappeared");
|
|
22761
|
-
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
22762
|
-
steps: [],
|
|
22763
|
-
completedSteps: []
|
|
22764
|
-
};
|
|
22765
|
-
return success(changed.data.revision, workspace.data.revision, {
|
|
22766
|
-
taskId: input.taskId,
|
|
22767
|
-
decisionId: input.decisionId,
|
|
22768
|
-
actionRef: input.actionRef,
|
|
22769
|
-
taskRevision: changed.data.revision,
|
|
22770
|
-
workspaceRevision: workspace.data.revision,
|
|
22771
|
-
completedSteps: [...workflow.completedSteps],
|
|
22772
|
-
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
22773
|
-
});
|
|
22774
|
-
}
|
|
22775
|
-
function settleAction(input) {
|
|
22776
|
-
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
22777
|
-
return failure("invalid_input", "action settlement preconditions are required");
|
|
22778
|
-
const authority = takeAuthority(input.authority, {
|
|
22779
|
-
owner: input.authorityOwner,
|
|
22780
|
-
store: input.store,
|
|
22781
|
-
root: input.store.root
|
|
22782
|
-
}, input.authorityCaller);
|
|
22783
|
-
if (!authority)
|
|
22784
|
-
return failure("permission_denied", "native settlement authority is not verified");
|
|
22785
|
-
if (!refSchema.safeParse(input.actionRef).success)
|
|
22786
|
-
return failure("invalid_input", "action reference is invalid");
|
|
22787
|
-
if (!authorityMatches(authority, {
|
|
22788
|
-
taskId: input.taskId,
|
|
22789
|
-
workspaceId: authority.workspaceId,
|
|
22790
|
-
decisionId: input.decisionId,
|
|
22791
|
-
actionRef: input.actionRef,
|
|
22792
|
-
taskRevision: input.taskRevision,
|
|
22793
|
-
workspaceRevision: input.workspaceRevision,
|
|
22794
|
-
outcome: input.outcome
|
|
22795
|
-
}))
|
|
22796
|
-
return failure("permission_denied", "native settlement authority is not verified");
|
|
22797
|
-
let outcomeResult = null;
|
|
22798
|
-
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
22799
|
-
const workspace = input.store.readWorkspace();
|
|
22800
|
-
if (!workspace.ok)
|
|
22801
|
-
return workspace;
|
|
22802
|
-
if (!workspace.data)
|
|
22803
|
-
return failure("not_found", "workspace not found");
|
|
22804
|
-
if (!authorityMatches(authority, {
|
|
22805
|
-
taskId: input.taskId,
|
|
22806
|
-
workspaceId: workspace.data.id,
|
|
22807
|
-
decisionId: input.decisionId,
|
|
22808
|
-
actionRef: input.actionRef,
|
|
22809
|
-
taskRevision: input.taskRevision,
|
|
22810
|
-
workspaceRevision: input.workspaceRevision,
|
|
22811
|
-
outcome: input.outcome
|
|
22812
|
-
}))
|
|
22813
|
-
return failure("permission_denied", "native settlement authority does not match context");
|
|
22814
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
22815
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
22816
|
-
expectedWorkspaceRevision: input.workspaceRevision,
|
|
22817
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
22818
|
-
});
|
|
22819
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
22820
|
-
if (!entry)
|
|
22821
|
-
return failure("not_found", "decision not found");
|
|
22822
|
-
if (!authorityDecisionMatches(authority, entry.data))
|
|
22823
|
-
return failure("permission_denied", "native settlement authority does not match decision");
|
|
22824
|
-
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
22825
|
-
return failure("permission_denied", "native settlement caller does not match approval");
|
|
22826
|
-
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
22827
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
22828
|
-
if (entry.data.consumption?.state === "uncertain")
|
|
22829
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
22830
|
-
operation: "settle_action",
|
|
22831
|
-
outcome: "unknown"
|
|
22832
|
-
});
|
|
22833
|
-
if (entry.data.consumption?.state !== "reserved")
|
|
22834
|
-
return failure("permission_denied", "action is not reserved");
|
|
22835
|
-
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
22836
|
-
return failure("permission_denied", "settlement reference does not match reservation");
|
|
22837
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
22838
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
22839
|
-
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
22840
|
-
return failure("recovery_required", "bounded action progress is missing");
|
|
22841
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
22842
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
22843
|
-
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
22844
|
-
if (input.outcome === "unknown") {
|
|
22845
|
-
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
22846
|
-
operation: "settle_action",
|
|
22847
|
-
outcome: "unknown"
|
|
22848
|
-
});
|
|
22849
|
-
const uncertain = {
|
|
22850
|
-
...entry.data,
|
|
22851
|
-
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
22852
|
-
};
|
|
22853
|
-
return success(mutation.revision, null, {
|
|
22854
|
-
...current,
|
|
22855
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
22856
|
-
});
|
|
22783
|
+
var gitContext = (workspaceRoot, paths = []) => {
|
|
22784
|
+
const cwd = workspaceRoot;
|
|
22785
|
+
let failure;
|
|
22786
|
+
const runHere = (args) => {
|
|
22787
|
+
const result = run(cwd, args);
|
|
22788
|
+
if (!failure && result.exitCode !== 0) {
|
|
22789
|
+
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
22857
22790
|
}
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
|
|
22791
|
+
return result.stdout;
|
|
22792
|
+
};
|
|
22793
|
+
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
22794
|
+
const status_short = runHere(["status", "--porcelain"]);
|
|
22795
|
+
const staged = [];
|
|
22796
|
+
const unstaged = [];
|
|
22797
|
+
const untracked = [];
|
|
22798
|
+
for (const line of status_short.split(`
|
|
22799
|
+
`).filter(Boolean)) {
|
|
22800
|
+
const code = line.slice(0, 2);
|
|
22801
|
+
const file = line.slice(3);
|
|
22802
|
+
if (code === "??") {
|
|
22803
|
+
untracked.push(file);
|
|
22804
|
+
} else {
|
|
22805
|
+
if (code[0] !== " " && code[0] !== "?")
|
|
22806
|
+
staged.push(file);
|
|
22807
|
+
if (code[1] !== " " && code[1] !== "?")
|
|
22808
|
+
unstaged.push(file);
|
|
22864
22809
|
}
|
|
22865
|
-
|
|
22866
|
-
|
|
22867
|
-
|
|
22868
|
-
|
|
22869
|
-
|
|
22870
|
-
|
|
22810
|
+
}
|
|
22811
|
+
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
22812
|
+
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
22813
|
+
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
22814
|
+
const stagedSet = new Set(staged);
|
|
22815
|
+
const unstagedSet = new Set(unstaged);
|
|
22816
|
+
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
22817
|
+
return {
|
|
22818
|
+
workspace_root: cwd,
|
|
22819
|
+
branch,
|
|
22820
|
+
status_short,
|
|
22821
|
+
staged,
|
|
22822
|
+
unstaged,
|
|
22823
|
+
untracked,
|
|
22824
|
+
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
22825
|
+
`),
|
|
22826
|
+
partial_staged: partial_staged.length > 0,
|
|
22827
|
+
partial_staged_files: partial_staged,
|
|
22828
|
+
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
22829
|
+
};
|
|
22830
|
+
};
|
|
22831
|
+
|
|
22832
|
+
// packages/workit-core/src/core/vcs-config.ts
|
|
22833
|
+
import fs5 from "node:fs";
|
|
22834
|
+
import path7 from "node:path";
|
|
22835
|
+
import { spawnSync } from "node:child_process";
|
|
22836
|
+
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
22837
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
|
|
22838
|
+
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
22839
|
+
var remoteProvider = (cwd) => {
|
|
22840
|
+
const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
22841
|
+
if (r.status !== 0)
|
|
22842
|
+
return null;
|
|
22843
|
+
const url = (r.stdout ?? "").trim();
|
|
22844
|
+
if (/github\.com[:/]/.test(url))
|
|
22845
|
+
return "github";
|
|
22846
|
+
if (/gitlab\.com[:/]/.test(url))
|
|
22847
|
+
return "gitlab";
|
|
22848
|
+
return null;
|
|
22849
|
+
};
|
|
22850
|
+
var readVcsConfig = () => {
|
|
22851
|
+
const file = vcsConfigPath();
|
|
22852
|
+
let raw;
|
|
22853
|
+
try {
|
|
22854
|
+
raw = fs5.readFileSync(file, "utf8");
|
|
22855
|
+
} catch {
|
|
22856
|
+
return { status: "missing", path: file, config: {} };
|
|
22857
|
+
}
|
|
22858
|
+
let parsed;
|
|
22859
|
+
try {
|
|
22860
|
+
parsed = JSON.parse(raw);
|
|
22861
|
+
} catch {
|
|
22862
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
22863
|
+
}
|
|
22864
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
22865
|
+
return { status: "valid", path: file, config: parsed };
|
|
22866
|
+
}
|
|
22867
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
22868
|
+
};
|
|
22869
|
+
function vcsConfig(mode, cwd) {
|
|
22870
|
+
const ws = resolveWorkspace(vcsCwd(cwd));
|
|
22871
|
+
const wsVcs = ws?.vcs ?? {};
|
|
22872
|
+
const wsYt = ws?.youtrack ?? {};
|
|
22873
|
+
const wsIssues = ws?.issues ?? {};
|
|
22874
|
+
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
22875
|
+
const root = vcsCwd(cwd);
|
|
22876
|
+
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
22877
|
+
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
22878
|
+
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
22879
|
+
const wp = ws?.branchPolicy ?? {};
|
|
22880
|
+
const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
|
|
22881
|
+
const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
22882
|
+
const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
|
|
22883
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
22884
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
22885
|
+
let issuesProvider = null;
|
|
22886
|
+
let linkOnPr = null;
|
|
22887
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
22888
|
+
issuesProvider = "github";
|
|
22889
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
22890
|
+
}
|
|
22891
|
+
if (mode === "resolve") {
|
|
22892
|
+
if (cfgStatus === "malformed")
|
|
22893
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
22894
|
+
return {
|
|
22895
|
+
ok: true,
|
|
22896
|
+
workspace_name: ws?.name ?? null,
|
|
22897
|
+
provider,
|
|
22898
|
+
defaultTargetBranch: defaultTarget,
|
|
22899
|
+
link_issues: linkIssues,
|
|
22900
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
22901
|
+
issues_provider: issuesProvider,
|
|
22902
|
+
link_on_pr: linkOnPr
|
|
22871
22903
|
};
|
|
22872
|
-
|
|
22873
|
-
|
|
22874
|
-
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
|
|
22878
|
-
|
|
22879
|
-
|
|
22880
|
-
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22904
|
+
}
|
|
22905
|
+
if (cfgStatus === "malformed") {
|
|
22906
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
22907
|
+
}
|
|
22908
|
+
if (cfgStatus === "missing") {
|
|
22909
|
+
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
22910
|
+
}
|
|
22911
|
+
if (provider === null) {
|
|
22912
|
+
return {
|
|
22913
|
+
ok: false,
|
|
22914
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
22915
|
+
configPath: cfgPath
|
|
22884
22916
|
};
|
|
22885
|
-
|
|
22886
|
-
|
|
22887
|
-
|
|
22888
|
-
|
|
22917
|
+
}
|
|
22918
|
+
const prov = cfg[provider] ?? {};
|
|
22919
|
+
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
22920
|
+
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path7.join(configDir(), `${provider}.token`));
|
|
22921
|
+
const tokenPath = path7.resolve(tokenFile);
|
|
22922
|
+
let tokenOk = false;
|
|
22923
|
+
if (fs5.existsSync(tokenPath)) {
|
|
22924
|
+
const token = fs5.readFileSync(tokenPath, "utf8").trim();
|
|
22925
|
+
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
22926
|
+
tokenOk = !placeholder;
|
|
22927
|
+
}
|
|
22928
|
+
const out = {
|
|
22929
|
+
ok: true,
|
|
22930
|
+
configPath: path7.resolve(cfgPath),
|
|
22931
|
+
provider,
|
|
22932
|
+
defaultTargetBranch: defaultTarget,
|
|
22933
|
+
pr: cfg.pr ?? {},
|
|
22934
|
+
tokenPath,
|
|
22935
|
+
tokenPresent: fs5.existsSync(tokenPath),
|
|
22936
|
+
tokenReady: tokenOk,
|
|
22937
|
+
workspace_name: ws?.name ?? null,
|
|
22938
|
+
link_issues: linkIssues,
|
|
22939
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
22940
|
+
issues_provider: issuesProvider,
|
|
22941
|
+
link_on_pr: linkOnPr
|
|
22942
|
+
};
|
|
22943
|
+
if (provider === "gitlab") {
|
|
22944
|
+
out.gitlab = {
|
|
22945
|
+
host: prov.host ?? "gitlab.com",
|
|
22946
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
22947
|
+
};
|
|
22948
|
+
} else if (provider === "github") {
|
|
22949
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
22950
|
+
}
|
|
22951
|
+
if (mode === "summary")
|
|
22952
|
+
delete out.tokenPath;
|
|
22953
|
+
return out;
|
|
22954
|
+
}
|
|
22955
|
+
function mergedPrStyle(limit = 6, cwd) {
|
|
22956
|
+
const cfg = vcsConfig("load", cwd);
|
|
22957
|
+
if (!cfg.ok || !cfg.tokenReady)
|
|
22958
|
+
return { ok: false, error: "vcs not configured" };
|
|
22959
|
+
const provider = cfg.provider;
|
|
22960
|
+
const token = fs5.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
22961
|
+
const examples = [];
|
|
22962
|
+
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
22963
|
+
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
22964
|
+
sections: desc.split(`
|
|
22965
|
+
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
22966
|
+
descriptionPreview: desc.slice(0, 600)
|
|
22967
|
+
});
|
|
22968
|
+
if (provider === "gitlab") {
|
|
22969
|
+
const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
22970
|
+
if (remote.status !== 0)
|
|
22971
|
+
return { ok: false, error: "no origin remote" };
|
|
22972
|
+
const url = (remote.stdout ?? "").trim();
|
|
22973
|
+
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
22974
|
+
if (!m)
|
|
22975
|
+
return { ok: false, error: "not a gitlab.com origin" };
|
|
22976
|
+
const project = m[1];
|
|
22977
|
+
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
22978
|
+
const run = (args) => spawnSync("glab", ["api", ...args], { cwd, encoding: "utf8", env });
|
|
22979
|
+
let r = run([
|
|
22980
|
+
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
|
|
22981
|
+
]);
|
|
22982
|
+
if (r.status !== 0) {
|
|
22983
|
+
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
22984
|
+
}
|
|
22985
|
+
if (r.status !== 0)
|
|
22986
|
+
return { ok: false, error: "could not list merge requests" };
|
|
22987
|
+
for (const mr of JSON.parse(r.stdout ?? "[]")) {
|
|
22988
|
+
const desc = String(mr.description ?? "").trim();
|
|
22989
|
+
examples.push({
|
|
22990
|
+
title: mr.title,
|
|
22991
|
+
url: mr.web_url,
|
|
22992
|
+
squash: mr.squash,
|
|
22993
|
+
...descInfo(desc, true)
|
|
22889
22994
|
});
|
|
22890
22995
|
}
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
if (outcomeResult)
|
|
22899
|
-
return outcomeResult;
|
|
22900
|
-
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
22901
|
-
}
|
|
22902
|
-
function reconcileAction(input) {
|
|
22903
|
-
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
22904
|
-
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
22905
|
-
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
22906
|
-
if (!authority || authority.kind !== "reconciliation")
|
|
22907
|
-
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
22908
|
-
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)
|
|
22909
|
-
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
22910
|
-
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
22911
|
-
const workspace = input.store.readWorkspace();
|
|
22912
|
-
if (!workspace.ok)
|
|
22913
|
-
return workspace;
|
|
22914
|
-
if (!workspace.data)
|
|
22915
|
-
return failure("not_found", "workspace not found");
|
|
22916
|
-
if (authority.workspaceId !== workspace.data.id)
|
|
22917
|
-
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
22918
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
22919
|
-
return failure("revision_conflict", "workspace revision does not match");
|
|
22920
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
22921
|
-
if (!entry || entry.data.purpose !== "action")
|
|
22922
|
-
return failure("not_found", "decision not found");
|
|
22923
|
-
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
22924
|
-
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
22925
|
-
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)
|
|
22926
|
-
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
22927
|
-
if (entry.data.consumption?.state !== "uncertain")
|
|
22928
|
-
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
22929
|
-
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
22930
|
-
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
22931
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
22932
|
-
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
22933
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
22934
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
22935
|
-
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
22936
|
-
if (input.outcome === "not_started") {
|
|
22937
|
-
return success(mutation.revision, null, {
|
|
22938
|
-
...current,
|
|
22939
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
22940
|
-
});
|
|
22996
|
+
} else if (provider === "github") {
|
|
22997
|
+
const r = spawnSync("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
22998
|
+
if (r.status !== 0)
|
|
22999
|
+
return { ok: false, error: "could not list pull requests" };
|
|
23000
|
+
for (const pr of JSON.parse(r.stdout ?? "[]")) {
|
|
23001
|
+
const desc = String(pr.body ?? "").trim();
|
|
23002
|
+
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
22941
23003
|
}
|
|
22942
|
-
|
|
22943
|
-
workflow.completed.push(currentStep);
|
|
22944
|
-
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
22945
|
-
const nextDecision = {
|
|
22946
|
-
...entry.data,
|
|
22947
|
-
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
22948
|
-
};
|
|
22949
|
-
const actionProgress = workflow.steps.length ? [
|
|
22950
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
22951
|
-
{
|
|
22952
|
-
decisionId: input.decisionId,
|
|
22953
|
-
steps: workflow.steps,
|
|
22954
|
-
completedSteps: workflow.completed
|
|
22955
|
-
}
|
|
22956
|
-
] : current.actionProgress ?? [];
|
|
22957
|
-
return success(mutation.revision, null, {
|
|
22958
|
-
...current,
|
|
22959
|
-
actionProgress,
|
|
22960
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
22961
|
-
});
|
|
22962
|
-
}, input.native?.now);
|
|
22963
|
-
if (!changed.ok)
|
|
22964
|
-
return changed;
|
|
22965
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
22966
|
-
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
22967
|
-
}
|
|
22968
|
-
var bindingCovers = scopeCovers;
|
|
22969
|
-
var verifyDecisionContent = verifyContentRefs;
|
|
22970
|
-
|
|
22971
|
-
// packages/workit-core/src/core/task-evaluation.ts
|
|
22972
|
-
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
22973
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
22974
|
-
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
22975
|
-
var canonicalPath = (value) => {
|
|
22976
|
-
if (typeof value !== "string" || !value)
|
|
22977
|
-
return null;
|
|
22978
|
-
if (value.includes("\\") || value.split("/").includes(".."))
|
|
22979
|
-
return null;
|
|
22980
|
-
const normalized = path6.posix.normalize(value);
|
|
22981
|
-
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
22982
|
-
return null;
|
|
22983
|
-
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
22984
|
-
};
|
|
22985
|
-
var canonicalScope = (scope) => {
|
|
22986
|
-
const paths = scope.paths.length ? scope.paths : ["."];
|
|
22987
|
-
const normalizedPaths = paths.map(canonicalPath);
|
|
22988
|
-
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
22989
|
-
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
22990
|
-
return null;
|
|
23004
|
+
}
|
|
22991
23005
|
return {
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
23006
|
+
ok: true,
|
|
23007
|
+
provider,
|
|
23008
|
+
count: examples.length,
|
|
23009
|
+
styleHints: [
|
|
23010
|
+
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
23011
|
+
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
23012
|
+
"Do not paste commit log or diff stat into the body"
|
|
23013
|
+
],
|
|
23014
|
+
examples
|
|
22995
23015
|
};
|
|
22996
|
-
}
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
|
|
23010
|
-
|
|
23011
|
-
|
|
23016
|
+
}
|
|
23017
|
+
|
|
23018
|
+
// packages/workit-core/src/core/branch.ts
|
|
23019
|
+
var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
23020
|
+
var verifyPushIdentity = (root, provider, account) => {
|
|
23021
|
+
if (!account || typeof account !== "string")
|
|
23022
|
+
return { ok: false, error: "push identity requires a configured area account" };
|
|
23023
|
+
const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
|
|
23024
|
+
const bin = provider === "gitlab" ? "glab" : "gh";
|
|
23025
|
+
let login;
|
|
23026
|
+
try {
|
|
23027
|
+
login = execFileSync2(bin, [...probe], {
|
|
23028
|
+
cwd: root,
|
|
23029
|
+
encoding: "utf8",
|
|
23030
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
23031
|
+
}).trim();
|
|
23032
|
+
} catch {
|
|
23033
|
+
return { ok: false, error: `${bin} identity could not be resolved` };
|
|
23012
23034
|
}
|
|
23013
|
-
|
|
23014
|
-
};
|
|
23015
|
-
var gitPaths = (root) => {
|
|
23016
|
-
const result = spawnSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
23017
|
-
cwd: root,
|
|
23018
|
-
encoding: "buffer"
|
|
23019
|
-
});
|
|
23020
|
-
if (result.status !== 0) {
|
|
23021
|
-
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
23035
|
+
if (!login || login.toLowerCase() !== account.toLowerCase())
|
|
23022
23036
|
return {
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
git: /not a git repository/i.test(stderr) ? false : true
|
|
23037
|
+
ok: false,
|
|
23038
|
+
error: `push identity ${login || "(unknown)"} does not match area account ${account}`
|
|
23026
23039
|
};
|
|
23027
|
-
}
|
|
23028
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
23029
|
-
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
23030
|
-
if (stagedDeleted.status !== 0) {
|
|
23031
|
-
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
23032
|
-
if (!/not a git repository/i.test(stderr))
|
|
23033
|
-
return { paths, uncertain: true, git: true };
|
|
23034
|
-
} else if (stagedDeleted.stdout) {
|
|
23035
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
23036
|
-
}
|
|
23037
|
-
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
23040
|
+
return { ok: true };
|
|
23038
23041
|
};
|
|
23039
|
-
|
|
23040
|
-
|
|
23042
|
+
|
|
23043
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
23044
|
+
var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
|
|
23045
|
+
var OPERATION_CLASS = {
|
|
23046
|
+
"git.branch_setup": "branch",
|
|
23047
|
+
"git.commit": "commit",
|
|
23048
|
+
"git.push": "push",
|
|
23049
|
+
"hosting.pull_request": "pr",
|
|
23050
|
+
"hosting.merge": "merge"
|
|
23051
|
+
};
|
|
23052
|
+
var operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null;
|
|
23053
|
+
var digestOf = (configDirPath) => {
|
|
23041
23054
|
try {
|
|
23042
|
-
|
|
23055
|
+
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
23056
|
+
return createHash3("sha256").update(raw).digest("hex");
|
|
23043
23057
|
} catch {
|
|
23044
|
-
return
|
|
23058
|
+
return null;
|
|
23045
23059
|
}
|
|
23046
|
-
|
|
23047
|
-
|
|
23048
|
-
|
|
23049
|
-
|
|
23050
|
-
|
|
23051
|
-
|
|
23052
|
-
|
|
23053
|
-
|
|
23054
|
-
|
|
23055
|
-
|
|
23056
|
-
continue;
|
|
23057
|
-
}
|
|
23058
|
-
const item = relative(root, target);
|
|
23059
|
-
output.add(item);
|
|
23060
|
-
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
23061
|
-
uncertain = walk(root, target, output) || uncertain;
|
|
23060
|
+
};
|
|
23061
|
+
var normalizeClasses = (value) => {
|
|
23062
|
+
if (value === true)
|
|
23063
|
+
return [...AUTO_CLASSES];
|
|
23064
|
+
if (!Array.isArray(value))
|
|
23065
|
+
return [];
|
|
23066
|
+
const seen = new Set;
|
|
23067
|
+
for (const item of value) {
|
|
23068
|
+
if (typeof item === "string" && AUTO_CLASSES.includes(item))
|
|
23069
|
+
seen.add(item);
|
|
23062
23070
|
}
|
|
23063
|
-
return
|
|
23071
|
+
return [...seen];
|
|
23064
23072
|
};
|
|
23065
|
-
var
|
|
23066
|
-
|
|
23067
|
-
|
|
23068
|
-
|
|
23069
|
-
|
|
23070
|
-
|
|
23071
|
-
let ancestor = target;
|
|
23072
|
-
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
23073
|
-
ancestor = path6.dirname(ancestor);
|
|
23074
|
-
try {
|
|
23075
|
-
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
23076
|
-
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` }] });
|
|
23077
|
-
} catch {
|
|
23078
|
-
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
23079
|
-
}
|
|
23080
|
-
roots.push(target);
|
|
23073
|
+
var resolveAutoApproval = (root, dir = configDir()) => {
|
|
23074
|
+
let match;
|
|
23075
|
+
try {
|
|
23076
|
+
match = resolveWorkspaceFrom(root, dir);
|
|
23077
|
+
} catch {
|
|
23078
|
+
return { status: "off" };
|
|
23081
23079
|
}
|
|
23082
|
-
|
|
23080
|
+
if (!match)
|
|
23081
|
+
return { status: "off" };
|
|
23082
|
+
const classes = normalizeClasses(match.autoApprove);
|
|
23083
|
+
if (classes.length === 0)
|
|
23084
|
+
return { status: "off" };
|
|
23085
|
+
return {
|
|
23086
|
+
status: "on",
|
|
23087
|
+
workspace: match.name,
|
|
23088
|
+
classes,
|
|
23089
|
+
account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
|
|
23090
|
+
configDigest: digestOf(dir)
|
|
23091
|
+
};
|
|
23083
23092
|
};
|
|
23084
|
-
|
|
23085
|
-
|
|
23093
|
+
var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
|
|
23094
|
+
var standingApprovalLive = (root, receipt, cls, dir) => {
|
|
23095
|
+
if (!isStandingReceipt(receipt))
|
|
23096
|
+
return false;
|
|
23097
|
+
if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
|
|
23098
|
+
return false;
|
|
23099
|
+
if (receipt.class !== cls)
|
|
23100
|
+
return false;
|
|
23101
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
23102
|
+
if (resolved.status !== "on")
|
|
23103
|
+
return false;
|
|
23104
|
+
if (resolved.workspace !== receipt.workspace)
|
|
23105
|
+
return false;
|
|
23106
|
+
return resolved.classes.includes(cls);
|
|
23107
|
+
};
|
|
23108
|
+
var standingReceiptFor = (root, workspace, cls, dir) => {
|
|
23109
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
23110
|
+
return {
|
|
23111
|
+
kind: "standing",
|
|
23112
|
+
workspace,
|
|
23113
|
+
class: cls,
|
|
23114
|
+
configDigest: resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null
|
|
23115
|
+
};
|
|
23116
|
+
};
|
|
23117
|
+
var verifyStandingApproval = (root, task, caller, binding) => {
|
|
23118
|
+
if (task.status !== "active")
|
|
23119
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
23120
|
+
const session = task.intent?.provenance?.session;
|
|
23121
|
+
if (!session || session.kind !== "host" || session.host !== caller.host || session.handle !== caller.actor)
|
|
23122
|
+
return failure("permission_denied", "standing approval requires the lead session");
|
|
23123
|
+
const standing = binding.standing;
|
|
23124
|
+
if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
|
|
23125
|
+
return failure("invalid_input", "standing approval needs a workspace and class");
|
|
23126
|
+
let operation;
|
|
23086
23127
|
try {
|
|
23087
|
-
|
|
23128
|
+
operation = typeof binding.approvedContent === "string" ? JSON.parse(binding.approvedContent).operation : null;
|
|
23088
23129
|
} catch {
|
|
23089
|
-
|
|
23130
|
+
operation = null;
|
|
23131
|
+
}
|
|
23132
|
+
const cls = operationAutoClass(operation);
|
|
23133
|
+
if (!cls || cls !== standing.class)
|
|
23134
|
+
return failure("permission_denied", "standing approval does not cover this operation");
|
|
23135
|
+
const resolved = resolveAutoApproval(root);
|
|
23136
|
+
if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
|
|
23137
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
23138
|
+
if (cls === "push") {
|
|
23139
|
+
const workspace = resolveWorkspaceFrom(root, configDir());
|
|
23140
|
+
const provider = workspace?.vcs?.provider;
|
|
23141
|
+
const account = resolved.account;
|
|
23142
|
+
if (!provider || !account)
|
|
23143
|
+
return failure("permission_denied", "auto-push requires a workspace provider and area account");
|
|
23144
|
+
const identity = verifyPushIdentity(root, provider, account);
|
|
23145
|
+
if (!identity.ok)
|
|
23146
|
+
return failure("permission_denied", identity.error);
|
|
23090
23147
|
}
|
|
23091
|
-
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23105
|
-
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
|
|
23109
|
-
|
|
23110
|
-
|
|
23111
|
-
|
|
23148
|
+
return success(null, null, {
|
|
23149
|
+
kind: "host_observed",
|
|
23150
|
+
host: caller.host,
|
|
23151
|
+
session: { kind: "host", host: caller.host, handle: caller.actor },
|
|
23152
|
+
workerId: null,
|
|
23153
|
+
receipts: [standingReceiptFor(root, standing.workspace, cls)]
|
|
23154
|
+
});
|
|
23155
|
+
};
|
|
23156
|
+
|
|
23157
|
+
// packages/workit-core/src/core/authority.ts
|
|
23158
|
+
var verifiedAuthorities = new WeakMap;
|
|
23159
|
+
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
23160
|
+
const token = {};
|
|
23161
|
+
verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
|
|
23162
|
+
return token;
|
|
23163
|
+
};
|
|
23164
|
+
var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
|
|
23165
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23166
|
+
var normalizeChainSteps = (steps) => {
|
|
23167
|
+
if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
|
|
23168
|
+
return null;
|
|
23169
|
+
const normalized = [];
|
|
23170
|
+
for (const step of steps) {
|
|
23171
|
+
if (typeof step === "string") {
|
|
23172
|
+
if (!step.trim())
|
|
23173
|
+
return null;
|
|
23174
|
+
normalized.push({ kind: "commit", message: step });
|
|
23112
23175
|
continue;
|
|
23113
23176
|
}
|
|
23114
|
-
if (!
|
|
23115
|
-
|
|
23116
|
-
|
|
23117
|
-
|
|
23118
|
-
|
|
23119
|
-
|
|
23120
|
-
|
|
23121
|
-
|
|
23122
|
-
for (const item of [...names].sort()) {
|
|
23123
|
-
if (!scopeMatches(item, normalizedScope))
|
|
23177
|
+
if (!isRecord(step))
|
|
23178
|
+
return null;
|
|
23179
|
+
if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
|
|
23180
|
+
normalized.push({ kind: "branch", target: step.branch });
|
|
23181
|
+
continue;
|
|
23182
|
+
}
|
|
23183
|
+
if (step.pr === true && Object.keys(step).length === 1) {
|
|
23184
|
+
normalized.push({ kind: "pr" });
|
|
23124
23185
|
continue;
|
|
23125
|
-
const target = path6.join(checkout, item);
|
|
23126
|
-
try {
|
|
23127
|
-
const stat = fs6.lstatSync(target);
|
|
23128
|
-
if (stat.isSymbolicLink()) {
|
|
23129
|
-
const link = fs6.readlinkSync(target);
|
|
23130
|
-
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
23131
|
-
} else if (stat.isFile()) {
|
|
23132
|
-
files.push({
|
|
23133
|
-
path: item,
|
|
23134
|
-
kind: "file",
|
|
23135
|
-
digest: digestBytes3(fs6.readFileSync(target)),
|
|
23136
|
-
executable: (stat.mode & 73) !== 0
|
|
23137
|
-
});
|
|
23138
|
-
} else if (!stat.isDirectory()) {
|
|
23139
|
-
completeness = "uncertain";
|
|
23140
|
-
} else if (git.git && git.paths.includes(item)) {
|
|
23141
|
-
completeness = "uncertain";
|
|
23142
|
-
}
|
|
23143
|
-
} catch (error) {
|
|
23144
|
-
if (error?.code === "ENOENT") {
|
|
23145
|
-
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
23146
|
-
} else
|
|
23147
|
-
completeness = "uncertain";
|
|
23148
23186
|
}
|
|
23187
|
+
return null;
|
|
23149
23188
|
}
|
|
23150
|
-
const
|
|
23151
|
-
|
|
23152
|
-
|
|
23153
|
-
|
|
23154
|
-
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
23155
|
-
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
23156
|
-
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
23157
|
-
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
23158
|
-
if (values.some(({ value }) => value === null))
|
|
23159
|
-
completeness = "uncertain";
|
|
23160
|
-
const headResult = spawnSync("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
23161
|
-
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
23162
|
-
completeness = "uncertain";
|
|
23163
|
-
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
23164
|
-
const partial = {
|
|
23165
|
-
id: "0".repeat(64),
|
|
23166
|
-
scope: normalizedScope,
|
|
23167
|
-
completeness,
|
|
23168
|
-
files,
|
|
23169
|
-
environment: values,
|
|
23170
|
-
head
|
|
23171
|
-
};
|
|
23172
|
-
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
23173
|
-
const parsed = candidateSchema.safeParse(candidate);
|
|
23174
|
-
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
23175
|
-
}
|
|
23176
|
-
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
23177
|
-
var changedPaths = (before, after) => {
|
|
23178
|
-
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
23179
|
-
const left = fileMap(before);
|
|
23180
|
-
const right = fileMap(after);
|
|
23181
|
-
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
23182
|
-
if (before.head !== after.head)
|
|
23183
|
-
changed.push(".");
|
|
23184
|
-
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
23185
|
-
changed.push(".");
|
|
23186
|
-
return changed;
|
|
23189
|
+
const keys = normalized.map(chainStepKey);
|
|
23190
|
+
if (new Set(keys).size !== keys.length)
|
|
23191
|
+
return null;
|
|
23192
|
+
return normalized;
|
|
23187
23193
|
};
|
|
23188
|
-
var
|
|
23189
|
-
|
|
23194
|
+
var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
23195
|
+
var parseSteps = (content) => {
|
|
23196
|
+
let value;
|
|
23197
|
+
try {
|
|
23198
|
+
value = JSON.parse(content);
|
|
23199
|
+
} catch {
|
|
23200
|
+
return [];
|
|
23201
|
+
}
|
|
23202
|
+
if (typeof value !== "object" || value === null || !("steps" in value))
|
|
23203
|
+
return [];
|
|
23204
|
+
const steps = value.steps;
|
|
23205
|
+
if (!Array.isArray(steps))
|
|
23190
23206
|
return null;
|
|
23191
|
-
|
|
23207
|
+
const normalized = normalizeChainSteps(steps);
|
|
23208
|
+
return normalized;
|
|
23192
23209
|
};
|
|
23193
|
-
var
|
|
23194
|
-
const
|
|
23195
|
-
return
|
|
23210
|
+
var decisionMatches = (entry, purpose, binding) => {
|
|
23211
|
+
const decision = entry.data;
|
|
23212
|
+
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);
|
|
23196
23213
|
};
|
|
23197
|
-
var
|
|
23198
|
-
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
return {
|
|
23204
|
-
evidenceId: entry.id,
|
|
23205
|
-
status: "failed",
|
|
23206
|
-
reason: "failed evidence remains historical"
|
|
23207
|
-
};
|
|
23208
|
-
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
23209
|
-
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
23210
|
-
return {
|
|
23211
|
-
evidenceId: entry.id,
|
|
23212
|
-
status: "stale",
|
|
23213
|
-
reason: "check or review evidence lacks start and end candidates"
|
|
23214
|
-
};
|
|
23215
|
-
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
23216
|
-
return {
|
|
23217
|
-
evidenceId: entry.id,
|
|
23218
|
-
status: "stale",
|
|
23219
|
-
reason: "candidate changed during the check or review"
|
|
23220
|
-
};
|
|
23221
|
-
}
|
|
23222
|
-
if (!evidence.candidateId || !current)
|
|
23223
|
-
return {
|
|
23224
|
-
evidenceId: entry.id,
|
|
23225
|
-
status: evidence.result,
|
|
23226
|
-
reason: "evidence has no candidate binding"
|
|
23227
|
-
};
|
|
23228
|
-
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
23229
|
-
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
23230
|
-
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
23231
|
-
if (observed.id === current.id)
|
|
23232
|
-
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
23233
|
-
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
23234
|
-
const changed = changedPaths(observed, current);
|
|
23235
|
-
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
23236
|
-
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
23237
|
-
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
23238
|
-
return {
|
|
23239
|
-
evidenceId: entry.id,
|
|
23240
|
-
status: evidence.result,
|
|
23241
|
-
reason: "candidate changed outside evidence scope"
|
|
23242
|
-
};
|
|
23243
|
-
});
|
|
23244
|
-
}
|
|
23245
|
-
var scopeCovers2 = scopeCovers;
|
|
23246
|
-
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
23247
|
-
const pin = assignment.candidateId ?? null;
|
|
23248
|
-
if (pin === null)
|
|
23249
|
-
return true;
|
|
23250
|
-
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
23214
|
+
var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
23215
|
+
var validProvenance = (value, caller) => {
|
|
23216
|
+
if (!provenanceSchema.safeParse(value).success)
|
|
23217
|
+
return false;
|
|
23218
|
+
const provenance = value;
|
|
23219
|
+
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);
|
|
23251
23220
|
};
|
|
23252
|
-
var
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23221
|
+
var verifyNativeDecision = (verifier, input, binding) => {
|
|
23222
|
+
if (!verifier)
|
|
23223
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
23224
|
+
const result = verifier.verifyDecision(input);
|
|
23225
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
23226
|
+
return failure("permission_denied", "native decision observation was not attested");
|
|
23227
|
+
return success(null, null, trustedAuthority("decision", result.data, {
|
|
23228
|
+
taskId: input.expected.taskId,
|
|
23229
|
+
workspaceId: input.expected.workspaceId,
|
|
23230
|
+
purpose: input.expected.purpose,
|
|
23231
|
+
response: input.expected.response,
|
|
23232
|
+
binding: input.expected.binding,
|
|
23233
|
+
bindingBytes: input.expected.bindingBytes,
|
|
23234
|
+
digest: input.expected.digest,
|
|
23235
|
+
requirementIds: [...input.expected.requirementIds]
|
|
23236
|
+
}, binding, input.caller));
|
|
23259
23237
|
};
|
|
23260
|
-
var
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
|
|
23264
|
-
if (
|
|
23265
|
-
return
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
|
|
23238
|
+
var verifyNativeAction = (verifier, input, binding) => {
|
|
23239
|
+
if (!verifier)
|
|
23240
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
23241
|
+
const result = verifier.verifyAction(input);
|
|
23242
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
23243
|
+
return failure("permission_denied", "native action observation was not attested");
|
|
23244
|
+
return success(null, null, trustedAuthority("action", result.data, {
|
|
23245
|
+
taskId: input.expected.taskId,
|
|
23246
|
+
workspaceId: input.expected.workspaceId,
|
|
23247
|
+
decisionId: input.expected.decisionId,
|
|
23248
|
+
actionRef: input.expected.actionRef,
|
|
23249
|
+
taskRevision: input.expected.taskRevision,
|
|
23250
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
23251
|
+
outcome: input.expected.outcome,
|
|
23252
|
+
purpose: input.expected.decision.purpose,
|
|
23253
|
+
response: input.expected.decision.response,
|
|
23254
|
+
binding: input.expected.decision.binding,
|
|
23255
|
+
digest: input.expected.decision.digest,
|
|
23256
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
23257
|
+
}, binding, input.caller));
|
|
23275
23258
|
};
|
|
23276
|
-
var
|
|
23277
|
-
if (
|
|
23278
|
-
return "
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23259
|
+
var verifyNativeReconciliation = (verifier, input, binding) => {
|
|
23260
|
+
if (!verifier?.verifyReconciliation)
|
|
23261
|
+
return failure("permission_denied", "native reconciliation authority is unavailable");
|
|
23262
|
+
const result = verifier.verifyReconciliation(input);
|
|
23263
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
23264
|
+
return failure("permission_denied", "native reconciliation observation was not attested");
|
|
23265
|
+
return success(null, null, trustedAuthority("reconciliation", result.data, {
|
|
23266
|
+
taskId: input.expected.taskId,
|
|
23267
|
+
workspaceId: input.expected.workspaceId,
|
|
23268
|
+
decisionId: input.expected.decisionId,
|
|
23269
|
+
actionRef: input.expected.actionRef,
|
|
23270
|
+
taskRevision: input.expected.taskRevision,
|
|
23271
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
23272
|
+
outcome: input.expected.outcome,
|
|
23273
|
+
evidenceDigest: input.expected.evidenceDigest,
|
|
23274
|
+
step: input.expected.step,
|
|
23275
|
+
purpose: input.expected.decision.purpose,
|
|
23276
|
+
response: input.expected.decision.response,
|
|
23277
|
+
binding: input.expected.decision.binding,
|
|
23278
|
+
digest: input.expected.decision.digest,
|
|
23279
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
23280
|
+
}, binding, input.caller));
|
|
23290
23281
|
};
|
|
23291
|
-
|
|
23292
|
-
const
|
|
23293
|
-
|
|
23294
|
-
|
|
23295
|
-
|
|
23296
|
-
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
|
|
23300
|
-
|
|
23301
|
-
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
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));
|
|
23313
|
-
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
23314
|
-
});
|
|
23315
|
-
if (passed.length) {
|
|
23316
|
-
const currentId = candidate?.id ?? null;
|
|
23317
|
-
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;
|
|
23318
|
-
if (withRed.length)
|
|
23319
|
-
return {
|
|
23320
|
-
requirementId: requirement.id,
|
|
23321
|
-
status: "satisfied",
|
|
23322
|
-
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
23323
|
-
decisionIds: [],
|
|
23324
|
-
reason: "fresh applicable evidence passed"
|
|
23325
|
-
};
|
|
23326
|
-
if (requirement.dimension === "testing")
|
|
23327
|
-
return {
|
|
23328
|
-
requirementId: requirement.id,
|
|
23329
|
-
status: "unsatisfied",
|
|
23330
|
-
evidenceIds: [],
|
|
23331
|
-
decisionIds: [],
|
|
23332
|
-
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
23333
|
-
};
|
|
23334
|
-
}
|
|
23335
|
-
if (requirement.dimension === "delegation") {
|
|
23336
|
-
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
23337
|
-
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))));
|
|
23338
|
-
if (reconciled)
|
|
23339
|
-
return {
|
|
23340
|
-
requirementId: requirement.id,
|
|
23341
|
-
status: "satisfied",
|
|
23342
|
-
evidenceIds: [],
|
|
23343
|
-
decisionIds: [],
|
|
23344
|
-
reason: "bounded helper completed and its result is recorded"
|
|
23345
|
-
};
|
|
23346
|
-
}
|
|
23347
|
-
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
23348
|
-
if (decisions.length)
|
|
23349
|
-
return {
|
|
23350
|
-
requirementId: requirement.id,
|
|
23351
|
-
status: "satisfied",
|
|
23352
|
-
evidenceIds: [],
|
|
23353
|
-
decisionIds: decisions.map((decision) => decision.id),
|
|
23354
|
-
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
23355
|
-
};
|
|
23356
|
-
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
23357
|
-
if (limitations.length)
|
|
23358
|
-
return {
|
|
23359
|
-
requirementId: requirement.id,
|
|
23360
|
-
status: "accepted_limitation",
|
|
23361
|
-
evidenceIds: [],
|
|
23362
|
-
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
23363
|
-
reason: "an applicable approved limitation permits the missing evidence"
|
|
23364
|
-
};
|
|
23365
|
-
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
23366
|
-
if (!unavailable && related.length > 0 && !passed.length)
|
|
23367
|
-
return {
|
|
23368
|
-
requirementId: requirement.id,
|
|
23369
|
-
status: "unsatisfied",
|
|
23370
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
23371
|
-
decisionIds: [],
|
|
23372
|
-
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
23373
|
-
};
|
|
23374
|
-
return {
|
|
23375
|
-
requirementId: requirement.id,
|
|
23376
|
-
status: unavailable ? "unavailable" : "unsatisfied",
|
|
23377
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
23378
|
-
decisionIds: [],
|
|
23379
|
-
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
23380
|
-
};
|
|
23381
|
-
});
|
|
23282
|
+
var takeAuthority = (authority, binding, caller) => {
|
|
23283
|
+
const record = verifiedAuthorities.get(authority);
|
|
23284
|
+
verifiedAuthorities.delete(authority);
|
|
23285
|
+
if (!record)
|
|
23286
|
+
return null;
|
|
23287
|
+
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
23288
|
+
return null;
|
|
23289
|
+
return record;
|
|
23290
|
+
};
|
|
23291
|
+
var retireNativeAuthority = (authority) => {
|
|
23292
|
+
const record = verifiedAuthorities.get(authority);
|
|
23293
|
+
verifiedAuthorities.delete(authority);
|
|
23294
|
+
return record?.provenance ?? null;
|
|
23295
|
+
};
|
|
23296
|
+
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;
|
|
23297
|
+
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);
|
|
23298
|
+
var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
|
|
23299
|
+
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
23300
|
+
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
23301
|
+
return [];
|
|
23302
|
+
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);
|
|
23382
23303
|
}
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23304
|
+
var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
23305
|
+
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
23306
|
+
let root;
|
|
23307
|
+
try {
|
|
23308
|
+
root = fs6.realpathSync(checkoutRoot);
|
|
23309
|
+
} catch {
|
|
23310
|
+
return failure("permission_denied", "checkout root is unavailable");
|
|
23311
|
+
}
|
|
23312
|
+
for (const reference of binding.contentRefs) {
|
|
23313
|
+
if (reference.kind !== "file")
|
|
23314
|
+
continue;
|
|
23315
|
+
if (!reference.digest)
|
|
23316
|
+
return failure("invalid_input", "document references require a byte digest");
|
|
23317
|
+
const target = path8.resolve(root, reference.path);
|
|
23318
|
+
if (target !== root && !target.startsWith(`${root}${path8.sep}`))
|
|
23319
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
23320
|
+
fields: [
|
|
23321
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
23322
|
+
]
|
|
23323
|
+
});
|
|
23324
|
+
try {
|
|
23325
|
+
const relative = path8.relative(root, target);
|
|
23326
|
+
let current = root;
|
|
23327
|
+
for (const segment of relative.split(path8.sep)) {
|
|
23328
|
+
if (!segment)
|
|
23329
|
+
continue;
|
|
23330
|
+
current = path8.join(current, segment);
|
|
23331
|
+
const stat = fs6.lstatSync(current);
|
|
23332
|
+
const real = fs6.realpathSync(current);
|
|
23333
|
+
if (real !== root && !real.startsWith(`${root}${path8.sep}`))
|
|
23334
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
23335
|
+
fields: [
|
|
23336
|
+
{
|
|
23337
|
+
path: "contentRefs.path",
|
|
23338
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
23339
|
+
}
|
|
23340
|
+
]
|
|
23404
23341
|
});
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23342
|
+
if (stat.isSymbolicLink())
|
|
23343
|
+
return failure("invalid_input", "document reference uses a symlink");
|
|
23344
|
+
if (current === target && !stat.isFile())
|
|
23345
|
+
return failure("invalid_input", "document reference is not a regular file");
|
|
23346
|
+
if (current !== target && !stat.isDirectory())
|
|
23347
|
+
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
23408
23348
|
}
|
|
23409
|
-
if (
|
|
23410
|
-
|
|
23411
|
-
|
|
23412
|
-
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23349
|
+
if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
|
|
23350
|
+
return failure("permission_denied", "approved document bytes have changed");
|
|
23351
|
+
} catch {
|
|
23352
|
+
return failure("invalid_input", "approved document is unavailable");
|
|
23353
|
+
}
|
|
23354
|
+
}
|
|
23355
|
+
return success(null, null, null);
|
|
23356
|
+
};
|
|
23357
|
+
var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
|
|
23358
|
+
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);
|
|
23359
|
+
var validateAction = (store, task, workspaceId, input, authority) => {
|
|
23360
|
+
if (task.status !== "active")
|
|
23361
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
23362
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
23363
|
+
return failure("invalid_input", "action reference is invalid");
|
|
23364
|
+
const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23365
|
+
if (!entry)
|
|
23366
|
+
return failure("not_found", "decision not found");
|
|
23367
|
+
const decision = entry.data;
|
|
23368
|
+
if (decision.purpose !== "action")
|
|
23369
|
+
return failure("permission_denied", "decision is not an action approval");
|
|
23370
|
+
if (!authorityMatches(authority, {
|
|
23371
|
+
taskId: task.id,
|
|
23372
|
+
workspaceId,
|
|
23373
|
+
decisionId: input.decisionId,
|
|
23374
|
+
actionRef: input.actionRef,
|
|
23375
|
+
taskRevision: input.expectedRevision,
|
|
23376
|
+
workspaceRevision: input.expectedWorkspaceRevision,
|
|
23377
|
+
outcome: "reserve"
|
|
23378
|
+
}))
|
|
23379
|
+
return failure("permission_denied", "native reservation authority is not bound");
|
|
23380
|
+
if (!authorityDecisionMatches(authority, decision))
|
|
23381
|
+
return failure("permission_denied", "native reservation authority does not match decision");
|
|
23382
|
+
if (decision.response !== "approved")
|
|
23383
|
+
return failure("permission_denied", "decision was rejected");
|
|
23384
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
23385
|
+
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
23386
|
+
const receipts = entry.provenance.receipts;
|
|
23387
|
+
const standing = receipts.filter((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "standing");
|
|
23388
|
+
const questionBacked = receipts.some((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "host");
|
|
23389
|
+
if (!questionBacked && standing.length > 0) {
|
|
23390
|
+
let operation;
|
|
23391
|
+
try {
|
|
23392
|
+
operation = JSON.parse(decision.binding.approvedContent).operation;
|
|
23393
|
+
} catch {
|
|
23394
|
+
operation = null;
|
|
23395
|
+
}
|
|
23396
|
+
const cls = operationAutoClass(operation);
|
|
23397
|
+
if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
|
|
23398
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
23399
|
+
}
|
|
23400
|
+
if (decision.revoked)
|
|
23401
|
+
return failure("permission_denied", "decision is revoked");
|
|
23402
|
+
if (decision.digest !== decisionDigest(decision))
|
|
23403
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
23404
|
+
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
23405
|
+
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
23406
|
+
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
23407
|
+
return failure("permission_denied", "action binding does not match the approved decision");
|
|
23408
|
+
const content = verifyContentRefs(store, decision.binding);
|
|
23409
|
+
if (!content.ok)
|
|
23410
|
+
return content;
|
|
23411
|
+
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
23412
|
+
if (requestedSteps === null)
|
|
23413
|
+
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
23414
|
+
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
23415
|
+
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
23416
|
+
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
23417
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
23418
|
+
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
23419
|
+
return failure("permission_denied", "bounded action scope changed");
|
|
23420
|
+
return success(null, null, { entry, workflow });
|
|
23421
|
+
};
|
|
23422
|
+
function reserveAction(input) {
|
|
23423
|
+
if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
|
|
23424
|
+
return failure("invalid_input", "action reservation preconditions are required");
|
|
23425
|
+
const authority = takeAuthority(input.authority, {
|
|
23426
|
+
owner: input.authorityOwner,
|
|
23427
|
+
store: input.store,
|
|
23428
|
+
root: input.store.root
|
|
23429
|
+
}, input.authorityCaller);
|
|
23430
|
+
if (!authority)
|
|
23431
|
+
return failure("permission_denied", "native reservation authority is not verified");
|
|
23432
|
+
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
23433
|
+
const workspace = input.store.readWorkspace();
|
|
23434
|
+
if (!workspace.ok)
|
|
23435
|
+
return workspace;
|
|
23436
|
+
if (!workspace.data)
|
|
23437
|
+
return failure("not_found", "workspace not found");
|
|
23438
|
+
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
23439
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
23440
|
+
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
23441
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
23442
|
+
});
|
|
23443
|
+
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
23444
|
+
if (!valid.ok)
|
|
23445
|
+
return valid;
|
|
23446
|
+
const { entry, workflow } = valid.data;
|
|
23447
|
+
const existing = entry.data.consumption;
|
|
23448
|
+
if (existing?.state === "uncertain")
|
|
23449
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
23450
|
+
operation: "reserve_action",
|
|
23451
|
+
outcome: "unknown"
|
|
23452
|
+
});
|
|
23453
|
+
if (existing?.state === "consumed")
|
|
23454
|
+
return failure("permission_denied", "action approval is consumed");
|
|
23455
|
+
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
23456
|
+
return failure("permission_denied", "action approval is already reserved");
|
|
23457
|
+
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
23458
|
+
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
23459
|
+
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
23460
|
+
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
23461
|
+
return failure("permission_denied", "action step was already completed");
|
|
23462
|
+
const nextDecision = {
|
|
23463
|
+
...entry.data,
|
|
23464
|
+
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
23465
|
+
};
|
|
23466
|
+
const actionProgress = workflow.steps.length ? [
|
|
23467
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
23468
|
+
{
|
|
23469
|
+
decisionId: input.decisionId,
|
|
23470
|
+
steps: [...workflow.steps],
|
|
23471
|
+
completedSteps: [...workflow.completed]
|
|
23417
23472
|
}
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23421
|
-
|
|
23422
|
-
|
|
23423
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
23424
|
-
return !requirement || requirement.before === "close";
|
|
23425
|
-
});
|
|
23426
|
-
if (requestedOutcome !== "stopped" && blocking.length)
|
|
23427
|
-
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
23428
|
-
requirementIds: blocking.map((item) => item.requirementId),
|
|
23429
|
-
requirements: blocking.map((item) => {
|
|
23430
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
23431
|
-
return {
|
|
23432
|
-
requirementId: item.requirementId,
|
|
23433
|
-
ruleId: requirement?.ruleId ?? "unknown",
|
|
23434
|
-
reason: item.reason,
|
|
23435
|
-
satisfaction: requirement?.satisfaction ?? "",
|
|
23436
|
-
dependentAction: requirement?.dependentAction ?? null
|
|
23437
|
-
};
|
|
23438
|
-
})
|
|
23439
|
-
});
|
|
23440
|
-
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
23441
|
-
if (requestedOutcome === "verified" && accepted.length)
|
|
23442
|
-
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
23443
|
-
requirementIds: accepted.map((item) => item.requirementId)
|
|
23444
|
-
});
|
|
23445
|
-
const evidenceIds = [
|
|
23446
|
-
...new Set([
|
|
23447
|
-
...evaluations.flatMap((item) => item.evidenceIds),
|
|
23448
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
23449
|
-
])
|
|
23450
|
-
];
|
|
23451
|
-
const decisionIds = [
|
|
23452
|
-
...new Set([
|
|
23453
|
-
...evaluations.flatMap((item) => item.decisionIds),
|
|
23454
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
23455
|
-
])
|
|
23456
|
-
];
|
|
23457
|
-
return success(null, null, {
|
|
23458
|
-
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
23459
|
-
evidenceIds,
|
|
23460
|
-
decisionIds,
|
|
23461
|
-
requirementIds: evaluations.map((item) => item.requirementId)
|
|
23462
|
-
});
|
|
23463
|
-
}
|
|
23464
|
-
|
|
23465
|
-
// packages/workit-core/src/core/task-context.ts
|
|
23466
|
-
function reconcileResume(view, observations = []) {
|
|
23467
|
-
if (observations.length > 0)
|
|
23468
|
-
return failure("permission_denied", "worker observations require native host verification");
|
|
23469
|
-
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
23470
|
-
if (!candidate.ok)
|
|
23471
|
-
return candidate;
|
|
23472
|
-
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
23473
|
-
const blockers = [...view.task.progress.blockers];
|
|
23474
|
-
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
23475
|
-
blockers.push({
|
|
23476
|
-
reason: "worker state requires reconciliation",
|
|
23477
|
-
dependentAction: "resume",
|
|
23478
|
-
refs: []
|
|
23473
|
+
] : current.actionProgress ?? [];
|
|
23474
|
+
return success(mutation.revision, null, {
|
|
23475
|
+
...current,
|
|
23476
|
+
actionProgress,
|
|
23477
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
23479
23478
|
});
|
|
23480
|
-
|
|
23481
|
-
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
|
|
23485
|
-
|
|
23479
|
+
}, input.native?.now);
|
|
23480
|
+
if (!changed.ok)
|
|
23481
|
+
return changed;
|
|
23482
|
+
const workspace = input.store.readWorkspace();
|
|
23483
|
+
if (!workspace.ok || !workspace.data)
|
|
23484
|
+
return failure("recovery_required", "workspace disappeared after reservation");
|
|
23485
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23486
|
+
if (!entry)
|
|
23487
|
+
return failure("recovery_required", "reserved decision disappeared");
|
|
23488
|
+
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
23489
|
+
steps: [],
|
|
23490
|
+
completedSteps: []
|
|
23491
|
+
};
|
|
23492
|
+
return success(changed.data.revision, workspace.data.revision, {
|
|
23493
|
+
taskId: input.taskId,
|
|
23494
|
+
decisionId: input.decisionId,
|
|
23495
|
+
actionRef: input.actionRef,
|
|
23496
|
+
taskRevision: changed.data.revision,
|
|
23497
|
+
workspaceRevision: workspace.data.revision,
|
|
23498
|
+
completedSteps: [...workflow.completedSteps],
|
|
23499
|
+
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
23486
23500
|
});
|
|
23487
23501
|
}
|
|
23488
|
-
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
|
|
23496
|
-
|
|
23497
|
-
|
|
23498
|
-
|
|
23499
|
-
|
|
23500
|
-
|
|
23501
|
-
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23507
|
-
|
|
23508
|
-
|
|
23509
|
-
|
|
23502
|
+
function settleAction(input) {
|
|
23503
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
23504
|
+
return failure("invalid_input", "action settlement preconditions are required");
|
|
23505
|
+
const authority = takeAuthority(input.authority, {
|
|
23506
|
+
owner: input.authorityOwner,
|
|
23507
|
+
store: input.store,
|
|
23508
|
+
root: input.store.root
|
|
23509
|
+
}, input.authorityCaller);
|
|
23510
|
+
if (!authority)
|
|
23511
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
23512
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
23513
|
+
return failure("invalid_input", "action reference is invalid");
|
|
23514
|
+
if (!authorityMatches(authority, {
|
|
23515
|
+
taskId: input.taskId,
|
|
23516
|
+
workspaceId: authority.workspaceId,
|
|
23517
|
+
decisionId: input.decisionId,
|
|
23518
|
+
actionRef: input.actionRef,
|
|
23519
|
+
taskRevision: input.taskRevision,
|
|
23520
|
+
workspaceRevision: input.workspaceRevision,
|
|
23521
|
+
outcome: input.outcome
|
|
23522
|
+
}))
|
|
23523
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
23524
|
+
let outcomeResult = null;
|
|
23525
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
23526
|
+
const workspace = input.store.readWorkspace();
|
|
23527
|
+
if (!workspace.ok)
|
|
23528
|
+
return workspace;
|
|
23529
|
+
if (!workspace.data)
|
|
23530
|
+
return failure("not_found", "workspace not found");
|
|
23531
|
+
if (!authorityMatches(authority, {
|
|
23532
|
+
taskId: input.taskId,
|
|
23533
|
+
workspaceId: workspace.data.id,
|
|
23534
|
+
decisionId: input.decisionId,
|
|
23535
|
+
actionRef: input.actionRef,
|
|
23536
|
+
taskRevision: input.taskRevision,
|
|
23537
|
+
workspaceRevision: input.workspaceRevision,
|
|
23538
|
+
outcome: input.outcome
|
|
23539
|
+
}))
|
|
23540
|
+
return failure("permission_denied", "native settlement authority does not match context");
|
|
23541
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
23542
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
23543
|
+
expectedWorkspaceRevision: input.workspaceRevision,
|
|
23544
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
23545
|
+
});
|
|
23546
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23547
|
+
if (!entry)
|
|
23548
|
+
return failure("not_found", "decision not found");
|
|
23549
|
+
if (!authorityDecisionMatches(authority, entry.data))
|
|
23550
|
+
return failure("permission_denied", "native settlement authority does not match decision");
|
|
23551
|
+
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
23552
|
+
return failure("permission_denied", "native settlement caller does not match approval");
|
|
23553
|
+
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
23554
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
23555
|
+
if (entry.data.consumption?.state === "uncertain")
|
|
23556
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
23557
|
+
operation: "settle_action",
|
|
23558
|
+
outcome: "unknown"
|
|
23559
|
+
});
|
|
23560
|
+
if (entry.data.consumption?.state !== "reserved")
|
|
23561
|
+
return failure("permission_denied", "action is not reserved");
|
|
23562
|
+
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
23563
|
+
return failure("permission_denied", "settlement reference does not match reservation");
|
|
23564
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
23565
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
23566
|
+
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
23567
|
+
return failure("recovery_required", "bounded action progress is missing");
|
|
23568
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
23569
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
23570
|
+
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
23571
|
+
if (input.outcome === "unknown") {
|
|
23572
|
+
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
23573
|
+
operation: "settle_action",
|
|
23574
|
+
outcome: "unknown"
|
|
23575
|
+
});
|
|
23576
|
+
const uncertain = {
|
|
23577
|
+
...entry.data,
|
|
23578
|
+
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
23579
|
+
};
|
|
23580
|
+
return success(mutation.revision, null, {
|
|
23581
|
+
...current,
|
|
23582
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
23583
|
+
});
|
|
23584
|
+
}
|
|
23585
|
+
if (input.outcome === "not_started") {
|
|
23586
|
+
const released = { ...entry.data, consumption: null };
|
|
23587
|
+
return success(mutation.revision, null, {
|
|
23588
|
+
...current,
|
|
23589
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
|
|
23590
|
+
});
|
|
23591
|
+
}
|
|
23592
|
+
if (currentStep)
|
|
23593
|
+
workflow.completed.push(currentStep);
|
|
23594
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
23595
|
+
const settled = {
|
|
23596
|
+
...entry.data,
|
|
23597
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
23598
|
+
};
|
|
23599
|
+
const actionProgress = workflow.steps.length ? [
|
|
23600
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
23601
|
+
{
|
|
23602
|
+
decisionId: input.decisionId,
|
|
23603
|
+
steps: [...workflow.steps],
|
|
23604
|
+
completedSteps: [...workflow.completed]
|
|
23605
|
+
}
|
|
23606
|
+
] : current.actionProgress ?? [];
|
|
23607
|
+
const next = {
|
|
23608
|
+
...current,
|
|
23609
|
+
actionProgress,
|
|
23610
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
|
|
23510
23611
|
};
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
|
|
23514
|
-
|
|
23515
|
-
|
|
23516
|
-
const runHere = (args) => {
|
|
23517
|
-
const result = run(cwd, args);
|
|
23518
|
-
if (!failure && result.exitCode !== 0) {
|
|
23519
|
-
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
23612
|
+
if (!complete) {
|
|
23613
|
+
outcomeResult = success(mutation.revision, input.workspaceRevision, {
|
|
23614
|
+
...entry,
|
|
23615
|
+
data: settled
|
|
23616
|
+
});
|
|
23520
23617
|
}
|
|
23521
|
-
return
|
|
23522
|
-
};
|
|
23523
|
-
|
|
23524
|
-
|
|
23525
|
-
const
|
|
23526
|
-
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23530
|
-
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
23535
|
-
|
|
23536
|
-
|
|
23537
|
-
|
|
23538
|
-
|
|
23618
|
+
return success(mutation.revision, null, next);
|
|
23619
|
+
}, input.native?.now);
|
|
23620
|
+
if (!changed.ok)
|
|
23621
|
+
return changed;
|
|
23622
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23623
|
+
if (!entry)
|
|
23624
|
+
return failure("recovery_required", "settled decision disappeared");
|
|
23625
|
+
if (outcomeResult)
|
|
23626
|
+
return outcomeResult;
|
|
23627
|
+
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
23628
|
+
}
|
|
23629
|
+
function reconcileAction(input) {
|
|
23630
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
23631
|
+
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
23632
|
+
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
23633
|
+
if (!authority || authority.kind !== "reconciliation")
|
|
23634
|
+
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
23635
|
+
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)
|
|
23636
|
+
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
23637
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
23638
|
+
const workspace = input.store.readWorkspace();
|
|
23639
|
+
if (!workspace.ok)
|
|
23640
|
+
return workspace;
|
|
23641
|
+
if (!workspace.data)
|
|
23642
|
+
return failure("not_found", "workspace not found");
|
|
23643
|
+
if (authority.workspaceId !== workspace.data.id)
|
|
23644
|
+
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
23645
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
23646
|
+
return failure("revision_conflict", "workspace revision does not match");
|
|
23647
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23648
|
+
if (!entry || entry.data.purpose !== "action")
|
|
23649
|
+
return failure("not_found", "decision not found");
|
|
23650
|
+
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
23651
|
+
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
23652
|
+
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)
|
|
23653
|
+
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
23654
|
+
if (entry.data.consumption?.state !== "uncertain")
|
|
23655
|
+
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
23656
|
+
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
23657
|
+
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
23658
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
23659
|
+
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
23660
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
23661
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
23662
|
+
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
23663
|
+
if (input.outcome === "not_started") {
|
|
23664
|
+
return success(mutation.revision, null, {
|
|
23665
|
+
...current,
|
|
23666
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
23667
|
+
});
|
|
23539
23668
|
}
|
|
23540
|
-
|
|
23541
|
-
|
|
23542
|
-
|
|
23543
|
-
|
|
23544
|
-
|
|
23545
|
-
|
|
23546
|
-
|
|
23547
|
-
|
|
23548
|
-
|
|
23549
|
-
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23554
|
-
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
};
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
readFileSync as readFileSync4,
|
|
23569
|
-
readdirSync as readdirSync3,
|
|
23570
|
-
writeFileSync as writeFileSync2
|
|
23571
|
-
} from "node:fs";
|
|
23572
|
-
import os from "node:os";
|
|
23573
|
-
import path7 from "node:path";
|
|
23574
|
-
|
|
23575
|
-
// packages/workit-core/src/core/boundary.ts
|
|
23576
|
-
import { statSync } from "node:fs";
|
|
23577
|
-
var EVENT = {
|
|
23578
|
-
initialization: "initialization",
|
|
23579
|
-
provenance: "provenance",
|
|
23580
|
-
assets: "assets",
|
|
23581
|
-
configurationSource: "configuration_source",
|
|
23582
|
-
hooks: "hooks",
|
|
23583
|
-
mcpConnection: "mcp_connection",
|
|
23584
|
-
migration: "migration",
|
|
23585
|
-
installSteps: "install_steps",
|
|
23586
|
-
uncaughtFailure: "uncaught_failure",
|
|
23587
|
-
toolsFailed: "tools_failed",
|
|
23588
|
-
doctor: "doctor"
|
|
23589
|
-
};
|
|
23590
|
-
var errorDetail = (err) => {
|
|
23591
|
-
if (err instanceof Error) {
|
|
23592
|
-
return { error_name: err.name, error: err.message };
|
|
23593
|
-
}
|
|
23594
|
-
return { error_name: "unknown", error: String(err) };
|
|
23595
|
-
};
|
|
23596
|
-
var markSourcesLoaded = (files, loadedAt = Date.now()) => ({
|
|
23597
|
-
loadedAt,
|
|
23598
|
-
files: [...files]
|
|
23599
|
-
});
|
|
23600
|
-
var changedSourcesSinceLoad = (marker) => {
|
|
23601
|
-
const changed = [];
|
|
23602
|
-
for (const file of marker.files) {
|
|
23603
|
-
try {
|
|
23604
|
-
if (statSync(file).mtimeMs > marker.loadedAt)
|
|
23605
|
-
changed.push(file);
|
|
23606
|
-
} catch {}
|
|
23607
|
-
}
|
|
23608
|
-
return changed;
|
|
23609
|
-
};
|
|
23669
|
+
if (currentStep)
|
|
23670
|
+
workflow.completed.push(currentStep);
|
|
23671
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
23672
|
+
const nextDecision = {
|
|
23673
|
+
...entry.data,
|
|
23674
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
23675
|
+
};
|
|
23676
|
+
const actionProgress = workflow.steps.length ? [
|
|
23677
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
23678
|
+
{
|
|
23679
|
+
decisionId: input.decisionId,
|
|
23680
|
+
steps: workflow.steps,
|
|
23681
|
+
completedSteps: workflow.completed
|
|
23682
|
+
}
|
|
23683
|
+
] : current.actionProgress ?? [];
|
|
23684
|
+
return success(mutation.revision, null, {
|
|
23685
|
+
...current,
|
|
23686
|
+
actionProgress,
|
|
23687
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
23688
|
+
});
|
|
23689
|
+
}, input.native?.now);
|
|
23690
|
+
if (!changed.ok)
|
|
23691
|
+
return changed;
|
|
23692
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23693
|
+
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
23694
|
+
}
|
|
23695
|
+
var bindingCovers = scopeCovers;
|
|
23696
|
+
var verifyDecisionContent = verifyContentRefs;
|
|
23610
23697
|
|
|
23611
|
-
// packages/workit-core/src/core/
|
|
23612
|
-
var
|
|
23613
|
-
var
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23617
|
-
|
|
23618
|
-
"
|
|
23619
|
-
|
|
23620
|
-
|
|
23621
|
-
|
|
23622
|
-
var mergePreset = (preset, input = {}, current = {
|
|
23623
|
-
branchPolicy: { preset, allowed: [], protected: [] }
|
|
23624
|
-
}) => {
|
|
23625
|
-
const defs = PRESETS[preset];
|
|
23626
|
-
return {
|
|
23627
|
-
preset,
|
|
23628
|
-
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
23629
|
-
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
23630
|
-
};
|
|
23631
|
-
};
|
|
23632
|
-
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
|
|
23633
|
-
var migratedDir = null;
|
|
23634
|
-
var migrationFailed = false;
|
|
23635
|
-
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
23636
|
-
if (migratedDir === dir)
|
|
23637
|
-
return dir;
|
|
23638
|
-
if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
|
|
23639
|
-
migratedDir = dir;
|
|
23640
|
-
return dir;
|
|
23641
|
-
}
|
|
23642
|
-
const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
23643
|
-
if (!existsSync4(legacy)) {
|
|
23644
|
-
migratedDir = dir;
|
|
23645
|
-
return dir;
|
|
23646
|
-
}
|
|
23647
|
-
if (!migrationFailed && existsSync4(dir)) {
|
|
23648
|
-
migratedDir = dir;
|
|
23649
|
-
return dir;
|
|
23650
|
-
}
|
|
23651
|
-
migrationFailed = false;
|
|
23652
|
-
mkdirSync2(dir, { recursive: true });
|
|
23653
|
-
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
23654
|
-
for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
|
|
23655
|
-
const src = path7.join(legacy, entry.name);
|
|
23656
|
-
const dest = path7.join(dir, entry.name);
|
|
23657
|
-
if (existsSync4(dest))
|
|
23658
|
-
continue;
|
|
23659
|
-
try {
|
|
23660
|
-
if (entry.isDirectory())
|
|
23661
|
-
cpSync(src, dest, { recursive: true });
|
|
23662
|
-
else if (entry.isFile())
|
|
23663
|
-
copyFileSync(src, dest);
|
|
23664
|
-
} catch (err) {
|
|
23665
|
-
migrationFailed = true;
|
|
23666
|
-
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
23667
|
-
console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
|
|
23668
|
-
}
|
|
23669
|
-
}
|
|
23670
|
-
if (!migrationFailed)
|
|
23671
|
-
migratedDir = dir;
|
|
23672
|
-
return dir;
|
|
23673
|
-
};
|
|
23674
|
-
var configDir = () => ensureConfigDir();
|
|
23675
|
-
var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
23676
|
-
var DEFAULTS = {
|
|
23677
|
-
locale: "en",
|
|
23678
|
-
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
23679
|
-
timezone: "America/Santiago",
|
|
23680
|
-
branchPolicy: {
|
|
23681
|
-
preset: "gitflow",
|
|
23682
|
-
allowed: [...PRESETS.gitflow.allowed],
|
|
23683
|
-
protected: [...PRESETS.gitflow.protected]
|
|
23684
|
-
},
|
|
23685
|
-
commitPolicy: { preset: "conventional" }
|
|
23686
|
-
};
|
|
23687
|
-
var readSafe = (p) => {
|
|
23688
|
-
try {
|
|
23689
|
-
return readFileSync4(p, "utf8");
|
|
23690
|
-
} catch {
|
|
23698
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
23699
|
+
var digestBytes3 = (value) => createHash5("sha256").update(value).digest("hex");
|
|
23700
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path9.sep}`);
|
|
23701
|
+
var relative = (root, candidate) => path9.relative(root, candidate).split(path9.sep).join("/") || ".";
|
|
23702
|
+
var canonicalPath = (value) => {
|
|
23703
|
+
if (typeof value !== "string" || !value)
|
|
23704
|
+
return null;
|
|
23705
|
+
if (value.includes("\\") || value.split("/").includes(".."))
|
|
23706
|
+
return null;
|
|
23707
|
+
const normalized = path9.posix.normalize(value);
|
|
23708
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
23691
23709
|
return null;
|
|
23692
|
-
|
|
23710
|
+
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
23693
23711
|
};
|
|
23694
|
-
var
|
|
23695
|
-
|
|
23696
|
-
|
|
23697
|
-
|
|
23698
|
-
|
|
23699
|
-
|
|
23700
|
-
parsed = JSON.parse(raw);
|
|
23701
|
-
} catch {
|
|
23702
|
-
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
23703
|
-
}
|
|
23704
|
-
if (!isConfigObject(parsed)) {
|
|
23705
|
-
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
23706
|
-
}
|
|
23707
|
-
const input = parsed;
|
|
23708
|
-
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
23709
|
-
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
23710
|
-
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
23712
|
+
var canonicalScope = (scope) => {
|
|
23713
|
+
const paths = scope.paths.length ? scope.paths : ["."];
|
|
23714
|
+
const normalizedPaths = paths.map(canonicalPath);
|
|
23715
|
+
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
23716
|
+
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
23717
|
+
return null;
|
|
23711
23718
|
return {
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
locale,
|
|
23716
|
-
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
23717
|
-
timezone: input.timezone ?? DEFAULTS.timezone,
|
|
23718
|
-
branchPolicy: mergePreset(preset, {
|
|
23719
|
-
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
23720
|
-
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
23721
|
-
}, DEFAULTS),
|
|
23722
|
-
commitPolicy: {
|
|
23723
|
-
preset: commitPreset,
|
|
23724
|
-
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
23725
|
-
}
|
|
23726
|
-
}
|
|
23719
|
+
description: scope.description,
|
|
23720
|
+
paths: [...new Set(normalizedPaths)],
|
|
23721
|
+
exclusions: [...new Set(normalizedExclusions)]
|
|
23727
23722
|
};
|
|
23728
23723
|
};
|
|
23729
|
-
var
|
|
23730
|
-
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
const
|
|
23735
|
-
|
|
23736
|
-
throw new Error(result.error);
|
|
23737
|
-
return result.config ?? DEFAULTS;
|
|
23724
|
+
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
23725
|
+
var scopeMatches = (value, scope) => {
|
|
23726
|
+
const normalized = canonicalScope(scope);
|
|
23727
|
+
if (!normalized)
|
|
23728
|
+
return false;
|
|
23729
|
+
const paths = normalized.paths;
|
|
23730
|
+
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
23738
23731
|
};
|
|
23739
|
-
var
|
|
23740
|
-
|
|
23741
|
-
const
|
|
23742
|
-
|
|
23743
|
-
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
return
|
|
23748
|
-
preset,
|
|
23749
|
-
allowed,
|
|
23750
|
-
protected: new Set(merged.protected.map((p) => p.toLowerCase())),
|
|
23751
|
-
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
23752
|
-
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
23753
|
-
};
|
|
23732
|
+
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
23733
|
+
var compareCodeUnits2 = (left, right) => {
|
|
23734
|
+
const length = Math.min(left.length, right.length);
|
|
23735
|
+
for (let index = 0;index < length; index += 1) {
|
|
23736
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
23737
|
+
if (difference !== 0)
|
|
23738
|
+
return difference;
|
|
23739
|
+
}
|
|
23740
|
+
return left.length - right.length;
|
|
23754
23741
|
};
|
|
23755
|
-
var
|
|
23756
|
-
"
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
var readWorkspacesResult = (dir = configDir()) => {
|
|
23768
|
-
const file = path8.join(dir, "workspaces.json");
|
|
23769
|
-
let raw;
|
|
23770
|
-
try {
|
|
23771
|
-
raw = readFileSync5(file, "utf8");
|
|
23772
|
-
} catch {
|
|
23773
|
-
return { status: "missing", path: file, entries: [] };
|
|
23742
|
+
var gitPaths = (root) => {
|
|
23743
|
+
const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
23744
|
+
cwd: root,
|
|
23745
|
+
encoding: "buffer"
|
|
23746
|
+
});
|
|
23747
|
+
if (result.status !== 0) {
|
|
23748
|
+
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
23749
|
+
return {
|
|
23750
|
+
paths: [],
|
|
23751
|
+
uncertain: !/not a git repository/i.test(stderr),
|
|
23752
|
+
git: /not a git repository/i.test(stderr) ? false : true
|
|
23753
|
+
};
|
|
23774
23754
|
}
|
|
23775
|
-
|
|
23755
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")) : [];
|
|
23756
|
+
const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
23757
|
+
if (stagedDeleted.status !== 0) {
|
|
23758
|
+
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
23759
|
+
if (!/not a git repository/i.test(stderr))
|
|
23760
|
+
return { paths, uncertain: true, git: true };
|
|
23761
|
+
} else if (stagedDeleted.stdout) {
|
|
23762
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")));
|
|
23763
|
+
}
|
|
23764
|
+
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
23765
|
+
};
|
|
23766
|
+
var walk = (root, directory, output) => {
|
|
23767
|
+
let names;
|
|
23776
23768
|
try {
|
|
23777
|
-
|
|
23769
|
+
names = fs7.readdirSync(directory);
|
|
23778
23770
|
} catch {
|
|
23779
|
-
return
|
|
23771
|
+
return true;
|
|
23780
23772
|
}
|
|
23781
|
-
|
|
23782
|
-
|
|
23773
|
+
let uncertain = false;
|
|
23774
|
+
for (const name of names) {
|
|
23775
|
+
if (name === ".git" || name === ".workit")
|
|
23776
|
+
continue;
|
|
23777
|
+
const target = path9.join(directory, name);
|
|
23778
|
+
let stat;
|
|
23779
|
+
try {
|
|
23780
|
+
stat = fs7.lstatSync(target);
|
|
23781
|
+
} catch {
|
|
23782
|
+
uncertain = true;
|
|
23783
|
+
continue;
|
|
23784
|
+
}
|
|
23785
|
+
const item = relative(root, target);
|
|
23786
|
+
output.add(item);
|
|
23787
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
23788
|
+
uncertain = walk(root, target, output) || uncertain;
|
|
23783
23789
|
}
|
|
23784
|
-
|
|
23785
|
-
return {
|
|
23786
|
-
status: "valid",
|
|
23787
|
-
path: file,
|
|
23788
|
-
entries: Array.isArray(list) ? list : []
|
|
23789
|
-
};
|
|
23790
|
+
return uncertain;
|
|
23790
23791
|
};
|
|
23791
|
-
var
|
|
23792
|
-
|
|
23793
|
-
|
|
23794
|
-
|
|
23795
|
-
|
|
23796
|
-
|
|
23797
|
-
|
|
23798
|
-
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
|
|
23802
|
-
|
|
23803
|
-
|
|
23804
|
-
|
|
23805
|
-
if (out.endsWith("/")) {
|
|
23806
|
-
out = out.slice(0, -1);
|
|
23807
|
-
out += "(?:/.*)?";
|
|
23808
|
-
} else {
|
|
23809
|
-
out += ".*";
|
|
23810
|
-
}
|
|
23811
|
-
} else {
|
|
23812
|
-
out += ".*";
|
|
23813
|
-
}
|
|
23814
|
-
i++;
|
|
23815
|
-
}
|
|
23816
|
-
} else {
|
|
23817
|
-
out += "[^/]*";
|
|
23818
|
-
}
|
|
23819
|
-
} else {
|
|
23820
|
-
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
23792
|
+
var scopeRoots = (root, scope) => {
|
|
23793
|
+
const roots = [];
|
|
23794
|
+
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
23795
|
+
const target = path9.resolve(root, item);
|
|
23796
|
+
if (!inside(root, target))
|
|
23797
|
+
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` }] });
|
|
23798
|
+
let ancestor = target;
|
|
23799
|
+
while (!fs7.existsSync(ancestor) && ancestor !== root)
|
|
23800
|
+
ancestor = path9.dirname(ancestor);
|
|
23801
|
+
try {
|
|
23802
|
+
if (!inside(root, fs7.realpathSync(ancestor)))
|
|
23803
|
+
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` }] });
|
|
23804
|
+
} catch {
|
|
23805
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
23821
23806
|
}
|
|
23807
|
+
roots.push(target);
|
|
23822
23808
|
}
|
|
23823
|
-
return
|
|
23809
|
+
return success(null, null, roots);
|
|
23824
23810
|
};
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
const real = realpathSync4.native ?? realpathSync4;
|
|
23811
|
+
function captureCandidate(root, scope, environment = []) {
|
|
23812
|
+
let checkout;
|
|
23828
23813
|
try {
|
|
23829
|
-
|
|
23814
|
+
checkout = fs7.realpathSync(root);
|
|
23830
23815
|
} catch {
|
|
23831
|
-
return
|
|
23816
|
+
return failure("invalid_input", "candidate root does not exist");
|
|
23832
23817
|
}
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
const
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
23840
|
-
const
|
|
23841
|
-
|
|
23842
|
-
|
|
23843
|
-
|
|
23844
|
-
|
|
23845
|
-
for (const entry of loadWorkspacesFrom(dir)) {
|
|
23846
|
-
if (!entry || typeof entry !== "object")
|
|
23818
|
+
const normalizedScope = canonicalScope(scope);
|
|
23819
|
+
if (!normalizedScope)
|
|
23820
|
+
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" }] });
|
|
23821
|
+
const roots = scopeRoots(checkout, normalizedScope);
|
|
23822
|
+
if (!roots.ok)
|
|
23823
|
+
return roots;
|
|
23824
|
+
const git = gitPaths(checkout);
|
|
23825
|
+
const names = new Set;
|
|
23826
|
+
let uncertain = git.uncertain;
|
|
23827
|
+
for (const target of roots.data) {
|
|
23828
|
+
const item = relative(checkout, target);
|
|
23829
|
+
if (item === ".git" || item === ".workit")
|
|
23847
23830
|
continue;
|
|
23848
|
-
|
|
23849
|
-
if (
|
|
23831
|
+
names.add(item);
|
|
23832
|
+
if (!fs7.existsSync(target))
|
|
23850
23833
|
continue;
|
|
23851
|
-
|
|
23852
|
-
|
|
23853
|
-
|
|
23854
|
-
|
|
23855
|
-
|
|
23856
|
-
|
|
23857
|
-
|
|
23834
|
+
let stat;
|
|
23835
|
+
try {
|
|
23836
|
+
stat = fs7.lstatSync(target);
|
|
23837
|
+
} catch {
|
|
23838
|
+
uncertain = true;
|
|
23839
|
+
continue;
|
|
23840
|
+
}
|
|
23841
|
+
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
23842
|
+
uncertain = walk(checkout, target, names) || uncertain;
|
|
23843
|
+
}
|
|
23844
|
+
for (const item of git.paths)
|
|
23845
|
+
if (scopeMatches(item, normalizedScope))
|
|
23846
|
+
names.add(item);
|
|
23847
|
+
const files = [];
|
|
23848
|
+
let completeness = uncertain ? "uncertain" : "known";
|
|
23849
|
+
for (const item of [...names].sort()) {
|
|
23850
|
+
if (!scopeMatches(item, normalizedScope))
|
|
23851
|
+
continue;
|
|
23852
|
+
const target = path9.join(checkout, item);
|
|
23853
|
+
try {
|
|
23854
|
+
const stat = fs7.lstatSync(target);
|
|
23855
|
+
if (stat.isSymbolicLink()) {
|
|
23856
|
+
const link = fs7.readlinkSync(target);
|
|
23857
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
23858
|
+
} else if (stat.isFile()) {
|
|
23859
|
+
files.push({
|
|
23860
|
+
path: item,
|
|
23861
|
+
kind: "file",
|
|
23862
|
+
digest: digestBytes3(fs7.readFileSync(target)),
|
|
23863
|
+
executable: (stat.mode & 73) !== 0
|
|
23864
|
+
});
|
|
23865
|
+
} else if (!stat.isDirectory()) {
|
|
23866
|
+
completeness = "uncertain";
|
|
23867
|
+
} else if (git.git && git.paths.includes(item)) {
|
|
23868
|
+
completeness = "uncertain";
|
|
23869
|
+
}
|
|
23870
|
+
} catch (error) {
|
|
23871
|
+
if (error?.code === "ENOENT") {
|
|
23872
|
+
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
23873
|
+
} else
|
|
23874
|
+
completeness = "uncertain";
|
|
23858
23875
|
}
|
|
23859
23876
|
}
|
|
23860
|
-
|
|
23877
|
+
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) => ({
|
|
23878
|
+
name,
|
|
23879
|
+
value: environment[name] ?? null
|
|
23880
|
+
}));
|
|
23881
|
+
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
23882
|
+
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
23883
|
+
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
23884
|
+
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
23885
|
+
if (values.some(({ value }) => value === null))
|
|
23886
|
+
completeness = "uncertain";
|
|
23887
|
+
const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
23888
|
+
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
23889
|
+
completeness = "uncertain";
|
|
23890
|
+
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
23891
|
+
const partial = {
|
|
23892
|
+
id: "0".repeat(64),
|
|
23893
|
+
scope: normalizedScope,
|
|
23894
|
+
completeness,
|
|
23895
|
+
files,
|
|
23896
|
+
environment: values,
|
|
23897
|
+
head
|
|
23898
|
+
};
|
|
23899
|
+
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
23900
|
+
const parsed = candidateSchema.safeParse(candidate);
|
|
23901
|
+
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
23902
|
+
}
|
|
23903
|
+
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
23904
|
+
var changedPaths = (before, after) => {
|
|
23905
|
+
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
23906
|
+
const left = fileMap(before);
|
|
23907
|
+
const right = fileMap(after);
|
|
23908
|
+
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
23909
|
+
if (before.head !== after.head)
|
|
23910
|
+
changed.push(".");
|
|
23911
|
+
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
23912
|
+
changed.push(".");
|
|
23913
|
+
return changed;
|
|
23861
23914
|
};
|
|
23862
|
-
var
|
|
23863
|
-
|
|
23864
|
-
// packages/workit-core/src/core/vcs-config.ts
|
|
23865
|
-
import fs7 from "node:fs";
|
|
23866
|
-
import path9 from "node:path";
|
|
23867
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
23868
|
-
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
23869
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
|
|
23870
|
-
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
23871
|
-
var remoteProvider = (cwd) => {
|
|
23872
|
-
const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
23873
|
-
if (r.status !== 0)
|
|
23915
|
+
var sessionFromRef = (value) => {
|
|
23916
|
+
if (typeof value !== "object" || value === null || value.kind !== "host")
|
|
23874
23917
|
return null;
|
|
23875
|
-
|
|
23876
|
-
if (/github\.com[:/]/.test(url))
|
|
23877
|
-
return "github";
|
|
23878
|
-
if (/gitlab\.com[:/]/.test(url))
|
|
23879
|
-
return "gitlab";
|
|
23880
|
-
return null;
|
|
23918
|
+
return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
23881
23919
|
};
|
|
23882
|
-
var
|
|
23883
|
-
const
|
|
23884
|
-
|
|
23885
|
-
try {
|
|
23886
|
-
raw = fs7.readFileSync(file, "utf8");
|
|
23887
|
-
} catch {
|
|
23888
|
-
return { status: "missing", path: file, config: {} };
|
|
23889
|
-
}
|
|
23890
|
-
let parsed;
|
|
23891
|
-
try {
|
|
23892
|
-
parsed = JSON.parse(raw);
|
|
23893
|
-
} catch {
|
|
23894
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
23895
|
-
}
|
|
23896
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
23897
|
-
return { status: "valid", path: file, config: parsed };
|
|
23898
|
-
}
|
|
23899
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
23920
|
+
var sameSession = (left, right) => {
|
|
23921
|
+
const other = sessionFromRef(right);
|
|
23922
|
+
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
23900
23923
|
};
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
const
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
|
|
23914
|
-
|
|
23915
|
-
|
|
23916
|
-
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
|
|
23923
|
-
|
|
23924
|
-
|
|
23925
|
-
|
|
23924
|
+
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
23925
|
+
function evaluateEvidence(task, candidate) {
|
|
23926
|
+
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
23927
|
+
return task.evidence.map((entry) => {
|
|
23928
|
+
const evidence = entry.data;
|
|
23929
|
+
if (evidence.result === "failed")
|
|
23930
|
+
return {
|
|
23931
|
+
evidenceId: entry.id,
|
|
23932
|
+
status: "failed",
|
|
23933
|
+
reason: "failed evidence remains historical"
|
|
23934
|
+
};
|
|
23935
|
+
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
23936
|
+
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
23937
|
+
return {
|
|
23938
|
+
evidenceId: entry.id,
|
|
23939
|
+
status: "stale",
|
|
23940
|
+
reason: "check or review evidence lacks start and end candidates"
|
|
23941
|
+
};
|
|
23942
|
+
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
23943
|
+
return {
|
|
23944
|
+
evidenceId: entry.id,
|
|
23945
|
+
status: "stale",
|
|
23946
|
+
reason: "candidate changed during the check or review"
|
|
23947
|
+
};
|
|
23948
|
+
}
|
|
23949
|
+
if (!evidence.candidateId || !current)
|
|
23950
|
+
return {
|
|
23951
|
+
evidenceId: entry.id,
|
|
23952
|
+
status: evidence.result,
|
|
23953
|
+
reason: "evidence has no candidate binding"
|
|
23954
|
+
};
|
|
23955
|
+
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
23956
|
+
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
23957
|
+
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
23958
|
+
if (observed.id === current.id)
|
|
23959
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
23960
|
+
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
23961
|
+
const changed = changedPaths(observed, current);
|
|
23962
|
+
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
23963
|
+
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
23964
|
+
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
23926
23965
|
return {
|
|
23927
|
-
|
|
23928
|
-
|
|
23929
|
-
|
|
23930
|
-
defaultTargetBranch: defaultTarget,
|
|
23931
|
-
link_issues: linkIssues,
|
|
23932
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
23933
|
-
issues_provider: issuesProvider,
|
|
23934
|
-
link_on_pr: linkOnPr
|
|
23966
|
+
evidenceId: entry.id,
|
|
23967
|
+
status: evidence.result,
|
|
23968
|
+
reason: "candidate changed outside evidence scope"
|
|
23935
23969
|
};
|
|
23936
|
-
}
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
|
|
23943
|
-
|
|
23970
|
+
});
|
|
23971
|
+
}
|
|
23972
|
+
var scopeCovers2 = scopeCovers;
|
|
23973
|
+
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
23974
|
+
const pin = assignment.candidateId ?? null;
|
|
23975
|
+
if (pin === null)
|
|
23976
|
+
return true;
|
|
23977
|
+
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
23978
|
+
};
|
|
23979
|
+
var resolveBinding = (evidence, pin, currentCandidateId) => {
|
|
23980
|
+
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
23981
|
+
const fallback = bound ? pin ?? currentCandidateId : null;
|
|
23982
|
+
return {
|
|
23983
|
+
beforeCandidateId: evidence.beforeCandidateId ?? fallback,
|
|
23984
|
+
candidateId: evidence.candidateId ?? fallback
|
|
23985
|
+
};
|
|
23986
|
+
};
|
|
23987
|
+
var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
|
|
23988
|
+
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")));
|
|
23989
|
+
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 }));
|
|
23990
|
+
var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
|
|
23991
|
+
if (kind !== "passed")
|
|
23992
|
+
return false;
|
|
23993
|
+
if (dimension === "testing" || dimension === "verification")
|
|
23994
|
+
return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
|
|
23995
|
+
if (dimension === "review")
|
|
23996
|
+
return evidenceKind === "review";
|
|
23997
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
23998
|
+
return evidenceKind === "investigation";
|
|
23999
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
24000
|
+
return evidenceKind === "artifact";
|
|
24001
|
+
return false;
|
|
24002
|
+
};
|
|
24003
|
+
var expectedKinds = (dimension, ruleId) => {
|
|
24004
|
+
if (dimension === "testing")
|
|
24005
|
+
return "check";
|
|
24006
|
+
if (dimension === "verification")
|
|
24007
|
+
return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
|
|
24008
|
+
if (dimension === "review")
|
|
24009
|
+
return "review";
|
|
24010
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
24011
|
+
return "investigation";
|
|
24012
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
24013
|
+
return "artifact";
|
|
24014
|
+
if (dimension === "decisions")
|
|
24015
|
+
return "decision";
|
|
24016
|
+
return "worker report";
|
|
24017
|
+
};
|
|
24018
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
|
|
24019
|
+
const evidence = evaluateEvidence(task, candidate);
|
|
24020
|
+
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
24021
|
+
return task.policy.requirements.map((requirement) => ({
|
|
24022
|
+
requirementId: requirement.id,
|
|
24023
|
+
status: "unsatisfied",
|
|
24024
|
+
evidenceIds: [],
|
|
24025
|
+
decisionIds: [],
|
|
24026
|
+
reason: "stored policy version is unsupported; reassessment is required"
|
|
24027
|
+
}));
|
|
24028
|
+
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
24029
|
+
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
24030
|
+
const passed = related.filter(({ entry, evaluation }) => {
|
|
24031
|
+
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
24032
|
+
return false;
|
|
24033
|
+
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
24034
|
+
return false;
|
|
24035
|
+
if (requirement.dimension !== "review")
|
|
24036
|
+
return true;
|
|
24037
|
+
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
24038
|
+
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
24039
|
+
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));
|
|
24040
|
+
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
24041
|
+
});
|
|
24042
|
+
if (passed.length) {
|
|
24043
|
+
const currentId = candidate?.id ?? null;
|
|
24044
|
+
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;
|
|
24045
|
+
if (withRed.length)
|
|
24046
|
+
return {
|
|
24047
|
+
requirementId: requirement.id,
|
|
24048
|
+
status: "satisfied",
|
|
24049
|
+
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
24050
|
+
decisionIds: [],
|
|
24051
|
+
reason: "fresh applicable evidence passed"
|
|
24052
|
+
};
|
|
24053
|
+
if (requirement.dimension === "testing")
|
|
24054
|
+
return {
|
|
24055
|
+
requirementId: requirement.id,
|
|
24056
|
+
status: "unsatisfied",
|
|
24057
|
+
evidenceIds: [],
|
|
24058
|
+
decisionIds: [],
|
|
24059
|
+
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
24060
|
+
};
|
|
24061
|
+
}
|
|
24062
|
+
if (requirement.dimension === "delegation") {
|
|
24063
|
+
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
24064
|
+
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))));
|
|
24065
|
+
if (reconciled)
|
|
24066
|
+
return {
|
|
24067
|
+
requirementId: requirement.id,
|
|
24068
|
+
status: "satisfied",
|
|
24069
|
+
evidenceIds: [],
|
|
24070
|
+
decisionIds: [],
|
|
24071
|
+
reason: "bounded helper completed and its result is recorded"
|
|
24072
|
+
};
|
|
24073
|
+
}
|
|
24074
|
+
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
24075
|
+
if (decisions.length)
|
|
24076
|
+
return {
|
|
24077
|
+
requirementId: requirement.id,
|
|
24078
|
+
status: "satisfied",
|
|
24079
|
+
evidenceIds: [],
|
|
24080
|
+
decisionIds: decisions.map((decision) => decision.id),
|
|
24081
|
+
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
24082
|
+
};
|
|
24083
|
+
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
24084
|
+
if (limitations.length)
|
|
24085
|
+
return {
|
|
24086
|
+
requirementId: requirement.id,
|
|
24087
|
+
status: "accepted_limitation",
|
|
24088
|
+
evidenceIds: [],
|
|
24089
|
+
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
24090
|
+
reason: "an applicable approved limitation permits the missing evidence"
|
|
24091
|
+
};
|
|
24092
|
+
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
24093
|
+
if (!unavailable && related.length > 0 && !passed.length)
|
|
24094
|
+
return {
|
|
24095
|
+
requirementId: requirement.id,
|
|
24096
|
+
status: "unsatisfied",
|
|
24097
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
24098
|
+
decisionIds: [],
|
|
24099
|
+
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
24100
|
+
};
|
|
23944
24101
|
return {
|
|
23945
|
-
|
|
23946
|
-
|
|
23947
|
-
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
const prov = cfg[provider] ?? {};
|
|
23951
|
-
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
23952
|
-
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
|
|
23953
|
-
const tokenPath = path9.resolve(tokenFile);
|
|
23954
|
-
let tokenOk = false;
|
|
23955
|
-
if (fs7.existsSync(tokenPath)) {
|
|
23956
|
-
const token = fs7.readFileSync(tokenPath, "utf8").trim();
|
|
23957
|
-
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
23958
|
-
tokenOk = !placeholder;
|
|
23959
|
-
}
|
|
23960
|
-
const out = {
|
|
23961
|
-
ok: true,
|
|
23962
|
-
configPath: path9.resolve(cfgPath),
|
|
23963
|
-
provider,
|
|
23964
|
-
defaultTargetBranch: defaultTarget,
|
|
23965
|
-
pr: cfg.pr ?? {},
|
|
23966
|
-
tokenPath,
|
|
23967
|
-
tokenPresent: fs7.existsSync(tokenPath),
|
|
23968
|
-
tokenReady: tokenOk,
|
|
23969
|
-
workspace_name: ws?.name ?? null,
|
|
23970
|
-
link_issues: linkIssues,
|
|
23971
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
23972
|
-
issues_provider: issuesProvider,
|
|
23973
|
-
link_on_pr: linkOnPr
|
|
23974
|
-
};
|
|
23975
|
-
if (provider === "gitlab") {
|
|
23976
|
-
out.gitlab = {
|
|
23977
|
-
host: prov.host ?? "gitlab.com",
|
|
23978
|
-
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
24102
|
+
requirementId: requirement.id,
|
|
24103
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
24104
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
24105
|
+
decisionIds: [],
|
|
24106
|
+
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
23979
24107
|
};
|
|
23980
|
-
} else if (provider === "github") {
|
|
23981
|
-
out.github = { host: prov.host ?? "github.com" };
|
|
23982
|
-
}
|
|
23983
|
-
if (mode === "summary")
|
|
23984
|
-
delete out.tokenPath;
|
|
23985
|
-
return out;
|
|
23986
|
-
}
|
|
23987
|
-
function mergedPrStyle(limit = 6, cwd) {
|
|
23988
|
-
const cfg = vcsConfig("load", cwd);
|
|
23989
|
-
if (!cfg.ok || !cfg.tokenReady)
|
|
23990
|
-
return { ok: false, error: "vcs not configured" };
|
|
23991
|
-
const provider = cfg.provider;
|
|
23992
|
-
const token = fs7.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
23993
|
-
const examples = [];
|
|
23994
|
-
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
23995
|
-
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
23996
|
-
sections: desc.split(`
|
|
23997
|
-
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
23998
|
-
descriptionPreview: desc.slice(0, 600)
|
|
23999
24108
|
});
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24011
|
-
|
|
24012
|
-
|
|
24013
|
-
|
|
24014
|
-
|
|
24015
|
-
|
|
24016
|
-
|
|
24017
|
-
|
|
24018
|
-
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
|
|
24026
|
-
}
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
24031
|
-
|
|
24032
|
-
|
|
24033
|
-
|
|
24034
|
-
|
|
24109
|
+
}
|
|
24110
|
+
function evaluateClosure(requestedOutcome, view) {
|
|
24111
|
+
if (requestedOutcome !== "stopped" && !view.task.policy)
|
|
24112
|
+
return failure("requirements_unsatisfied", "task has not been assessed");
|
|
24113
|
+
if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
|
|
24114
|
+
return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
|
|
24115
|
+
const evaluations = view.requirements;
|
|
24116
|
+
const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
|
|
24117
|
+
if (requestedOutcome !== "stopped" && openFindings.length)
|
|
24118
|
+
return failure("requirements_unsatisfied", "open findings must be resolved before closure");
|
|
24119
|
+
if (requestedOutcome !== "stopped") {
|
|
24120
|
+
const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
|
|
24121
|
+
for (const entry of view.task.findings) {
|
|
24122
|
+
if (entry.data.disposition === "fixed") {
|
|
24123
|
+
const verified = entry.data.resolution?.evidenceIds.some((id) => {
|
|
24124
|
+
const evidence = view.task.evidence.find((item) => item.id === id);
|
|
24125
|
+
if (!evidence)
|
|
24126
|
+
return false;
|
|
24127
|
+
return findingVerificationPasses(entry.data.candidateId, {
|
|
24128
|
+
kind: evidence.data.kind,
|
|
24129
|
+
candidateId: evidence.data.candidateId,
|
|
24130
|
+
status: evidenceById.get(id) ?? "missing"
|
|
24131
|
+
});
|
|
24132
|
+
});
|
|
24133
|
+
if (!verified)
|
|
24134
|
+
return failure("requirements_unsatisfied", "fixed findings require current verification");
|
|
24135
|
+
}
|
|
24136
|
+
if (entry.data.disposition === "deferred") {
|
|
24137
|
+
const valid = entry.data.resolution?.decisionIds.some((id) => {
|
|
24138
|
+
const decisionEntry = view.task.decisions.find((item) => item.id === id);
|
|
24139
|
+
const decision = decisionEntry?.data;
|
|
24140
|
+
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)));
|
|
24141
|
+
});
|
|
24142
|
+
if (!valid)
|
|
24143
|
+
return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
24144
|
+
}
|
|
24035
24145
|
}
|
|
24036
24146
|
}
|
|
24037
|
-
|
|
24038
|
-
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
24043
|
-
|
|
24044
|
-
|
|
24045
|
-
|
|
24046
|
-
|
|
24047
|
-
|
|
24147
|
+
const blocking = evaluations.filter((item) => {
|
|
24148
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
24149
|
+
return false;
|
|
24150
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
24151
|
+
return !requirement || requirement.before === "close";
|
|
24152
|
+
});
|
|
24153
|
+
if (requestedOutcome !== "stopped" && blocking.length)
|
|
24154
|
+
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
24155
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
24156
|
+
requirements: blocking.map((item) => {
|
|
24157
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
24158
|
+
return {
|
|
24159
|
+
requirementId: item.requirementId,
|
|
24160
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
24161
|
+
reason: item.reason,
|
|
24162
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
24163
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
24164
|
+
};
|
|
24165
|
+
})
|
|
24166
|
+
});
|
|
24167
|
+
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
24168
|
+
if (requestedOutcome === "verified" && accepted.length)
|
|
24169
|
+
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
24170
|
+
requirementIds: accepted.map((item) => item.requirementId)
|
|
24171
|
+
});
|
|
24172
|
+
const evidenceIds = [
|
|
24173
|
+
...new Set([
|
|
24174
|
+
...evaluations.flatMap((item) => item.evidenceIds),
|
|
24175
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
24176
|
+
])
|
|
24177
|
+
];
|
|
24178
|
+
const decisionIds = [
|
|
24179
|
+
...new Set([
|
|
24180
|
+
...evaluations.flatMap((item) => item.decisionIds),
|
|
24181
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
24182
|
+
])
|
|
24183
|
+
];
|
|
24184
|
+
return success(null, null, {
|
|
24185
|
+
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
24186
|
+
evidenceIds,
|
|
24187
|
+
decisionIds,
|
|
24188
|
+
requirementIds: evaluations.map((item) => item.requirementId)
|
|
24189
|
+
});
|
|
24048
24190
|
}
|
|
24049
24191
|
|
|
24050
|
-
// packages/workit-core/src/core/
|
|
24051
|
-
|
|
24192
|
+
// packages/workit-core/src/core/task-context.ts
|
|
24193
|
+
function reconcileResume(view, observations = []) {
|
|
24194
|
+
if (observations.length > 0)
|
|
24195
|
+
return failure("permission_denied", "worker observations require native host verification");
|
|
24196
|
+
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
24197
|
+
if (!candidate.ok)
|
|
24198
|
+
return candidate;
|
|
24199
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
24200
|
+
const blockers = [...view.task.progress.blockers];
|
|
24201
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
24202
|
+
blockers.push({
|
|
24203
|
+
reason: "worker state requires reconciliation",
|
|
24204
|
+
dependentAction: "resume",
|
|
24205
|
+
refs: []
|
|
24206
|
+
});
|
|
24207
|
+
return success(null, null, {
|
|
24208
|
+
candidate: candidate.data,
|
|
24209
|
+
staleEvidenceIds,
|
|
24210
|
+
workerUpdates: [],
|
|
24211
|
+
blockers,
|
|
24212
|
+
reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
|
|
24213
|
+
});
|
|
24214
|
+
}
|
|
24215
|
+
var exportDigest = (bundle) => sha256({
|
|
24216
|
+
schemaVersion: bundle.schemaVersion,
|
|
24217
|
+
exportedAt: bundle.exportedAt,
|
|
24218
|
+
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
24219
|
+
task: bundle.task
|
|
24220
|
+
});
|
|
24052
24221
|
// packages/workit-core/src/core/external-action.ts
|
|
24053
24222
|
var externalActionSchema = discriminatedUnion2("operation", [
|
|
24054
24223
|
object4({
|
|
@@ -24082,6 +24251,13 @@ var externalActionSchema = discriminatedUnion2("operation", [
|
|
|
24082
24251
|
babysit: boolean5().optional()
|
|
24083
24252
|
}).strict()
|
|
24084
24253
|
}).strict(),
|
|
24254
|
+
object4({
|
|
24255
|
+
operation: literal3("hosting.merge"),
|
|
24256
|
+
payload: object4({
|
|
24257
|
+
target_branch: string5().min(1).optional(),
|
|
24258
|
+
source_branch: string5().min(1).optional()
|
|
24259
|
+
}).strict()
|
|
24260
|
+
}).strict(),
|
|
24085
24261
|
object4({
|
|
24086
24262
|
operation: literal3("youtrack.update"),
|
|
24087
24263
|
payload: object4({
|
|
@@ -25195,7 +25371,10 @@ class WorkitCore {
|
|
|
25195
25371
|
observeDecision(request, observation) {
|
|
25196
25372
|
return this.recordDecision(request, observation, true);
|
|
25197
25373
|
}
|
|
25198
|
-
|
|
25374
|
+
observeStandingDecision(request) {
|
|
25375
|
+
return this.recordDecision(request, undefined, true, true);
|
|
25376
|
+
}
|
|
25377
|
+
recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
|
|
25199
25378
|
const root = this.contextRootError();
|
|
25200
25379
|
if (!root.ok)
|
|
25201
25380
|
return root;
|
|
@@ -25210,8 +25389,10 @@ class WorkitCore {
|
|
|
25210
25389
|
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
25211
25390
|
if (stated && !input.binding?.statedChoice)
|
|
25212
25391
|
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
25213
|
-
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
25392
|
+
if (nativeRequired && nativeObservation === undefined && !stated && !standing)
|
|
25214
25393
|
return failure("permission_denied", "native decision observation is required");
|
|
25394
|
+
if (standing && (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved"))
|
|
25395
|
+
return failure("invalid_input", "standing approvals need an approved action binding with a standing rule");
|
|
25215
25396
|
const task = this.store.readTask(input.taskId);
|
|
25216
25397
|
if (!task.ok)
|
|
25217
25398
|
return task;
|
|
@@ -25243,7 +25424,10 @@ class WorkitCore {
|
|
|
25243
25424
|
consumption: null
|
|
25244
25425
|
};
|
|
25245
25426
|
const data = { ...base, digest: decisionDigest(base) };
|
|
25246
|
-
const
|
|
25427
|
+
const standingApproval = standing && !stated ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding) : null;
|
|
25428
|
+
if (standingApproval && !standingApproval.ok)
|
|
25429
|
+
return standingApproval;
|
|
25430
|
+
const native = nativeRequired && !stated && !standingApproval ? verifyNativeDecision(this.context.nativeAuthority, {
|
|
25247
25431
|
observation: nativeObservation,
|
|
25248
25432
|
expected: {
|
|
25249
25433
|
taskId: task.data.id,
|
|
@@ -25274,7 +25458,7 @@ class WorkitCore {
|
|
|
25274
25458
|
const entry = {
|
|
25275
25459
|
id: newId(),
|
|
25276
25460
|
recordedAt: mutation.now,
|
|
25277
|
-
provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
25461
|
+
provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
25278
25462
|
data
|
|
25279
25463
|
};
|
|
25280
25464
|
return success(mutation.revision, null, {
|
|
@@ -26493,7 +26677,7 @@ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "gla
|
|
|
26493
26677
|
|
|
26494
26678
|
// packages/workit-core/src/core/repo-context.ts
|
|
26495
26679
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
26496
|
-
import { existsSync as existsSync6, readFileSync as
|
|
26680
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
26497
26681
|
import path12 from "node:path";
|
|
26498
26682
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
26499
26683
|
try {
|
|
@@ -26663,7 +26847,7 @@ var packageScripts = (cwd, keys) => {
|
|
|
26663
26847
|
if (!existsSync6(pkgPath))
|
|
26664
26848
|
return [];
|
|
26665
26849
|
try {
|
|
26666
|
-
const pkg = JSON.parse(
|
|
26850
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
26667
26851
|
return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
|
|
26668
26852
|
} catch {
|
|
26669
26853
|
return [];
|
|
@@ -26697,7 +26881,7 @@ var documentationFiles = (cwd) => {
|
|
|
26697
26881
|
var readTrimmed = (file, maxLines) => {
|
|
26698
26882
|
if (!existsSync6(file))
|
|
26699
26883
|
return "";
|
|
26700
|
-
const lines =
|
|
26884
|
+
const lines = readFileSync7(file, "utf8").split(`
|
|
26701
26885
|
`);
|
|
26702
26886
|
return lines.slice(0, maxLines).join(`
|
|
26703
26887
|
`);
|
|
@@ -26885,7 +27069,7 @@ function docsRefreshContext(root, range) {
|
|
|
26885
27069
|
const pkgPath = path12.join(cwd, "package.json");
|
|
26886
27070
|
if (existsSync6(pkgPath)) {
|
|
26887
27071
|
try {
|
|
26888
|
-
const pkg = JSON.parse(
|
|
27072
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
26889
27073
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
26890
27074
|
`;
|
|
26891
27075
|
} catch {}
|
|
@@ -27583,7 +27767,7 @@ var TOOL_CAPABILITIES = [
|
|
|
27583
27767
|
];
|
|
27584
27768
|
var VERSION = (() => {
|
|
27585
27769
|
try {
|
|
27586
|
-
const packageJson = JSON.parse(
|
|
27770
|
+
const packageJson = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8"));
|
|
27587
27771
|
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
27588
27772
|
} catch {
|
|
27589
27773
|
return "0.0.0";
|