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