@ricsam/r5d-worker 0.0.107 → 0.0.108
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/README.md +1 -1
- package/dist/cjs/main.cjs +97 -22
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/plan-lint.cjs +165 -0
- package/dist/cjs/plan-parser.cjs +127 -0
- package/dist/cjs/plan-paths.cjs +53 -0
- package/dist/mjs/main.mjs +96 -21
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/plan-lint.mjs +129 -0
- package/dist/mjs/plan-parser.mjs +94 -0
- package/dist/mjs/plan-paths.mjs +25 -0
- package/dist/types/main.d.ts +40 -1
- package/dist/types/plan-lint.d.ts +16 -0
- package/dist/types/plan-parser.d.ts +38 -0
- package/dist/types/plan-paths.d.ts +36 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Worker labels are mandatory and unique per user. Choose labels that describe hos
|
|
|
52
52
|
|
|
53
53
|
`r5d-worker start` keeps a lightweight supervisor process attached to the launching terminal or service. When both r5d CLIs are updated from User Settings, the connected runtime verifies the installed versions, exits with a reload signal, and the supervisor reconnects using the new worker package. Updates only run while the worker has no active commands or shells.
|
|
54
54
|
|
|
55
|
-
The supervisor also reconnects automatically after server rollouts and transient network failures. A brief bounded delay prevents a tight retry loop while the server is unavailable. Pressing Ctrl+C still stops the worker, and agent-managed processes retain their existing in-process recovery path while disconnected.
|
|
55
|
+
The supervisor also reconnects automatically after server rollouts and transient network failures. A brief bounded delay prevents a tight retry loop while the server is unavailable. Pressing Ctrl+C still stops the worker, and agent-managed processes retain their existing in-process recovery path while disconnected. In-process reconnects retain initialized project configurations alongside those processes, so an unchanged checkout can reconnect while a dev server is running. Changed layouts and checkouts that need initialization still wait for active commands to finish.
|
|
56
56
|
|
|
57
57
|
The runtime opens its control connection before recovering interrupted project snapshots. During recovery and initial project configuration it remains connected but non-dispatchable, reports its current phase plus branch/byte progress to the server, and prints the same progress locally. Recovery runs in a credential-sanitized helper process so the control loop can continue answering heartbeats. A durable `restoring` transaction state permits same-filesystem snapshots to be restored with one atomic directory move; cross-filesystem layouts retain the copy-and-fsync fallback.
|
|
58
58
|
|
package/dist/cjs/main.cjs
CHANGED
|
@@ -56,6 +56,9 @@ __export(main_exports, {
|
|
|
56
56
|
writeWorkerTextFile: () => writeWorkerTextFile
|
|
57
57
|
});
|
|
58
58
|
module.exports = __toCommonJS(main_exports);
|
|
59
|
+
var import_plan_paths = require("./plan-paths.cjs");
|
|
60
|
+
var import_plan_parser = require("./plan-parser.cjs");
|
|
61
|
+
var import_plan_lint = require("./plan-lint.cjs");
|
|
59
62
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
60
63
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
61
64
|
var import_node_crypto = require("node:crypto");
|
|
@@ -565,7 +568,7 @@ function targetPlanDir(planRoot, target) {
|
|
|
565
568
|
function planEnv(activePlanId) {
|
|
566
569
|
const env = {};
|
|
567
570
|
if (activePlanId) {
|
|
568
|
-
validatePlanId(activePlanId);
|
|
571
|
+
(0, import_plan_paths.validatePlanId)(activePlanId);
|
|
569
572
|
env.R5D_ACTIVE_PLAN_ID = activePlanId;
|
|
570
573
|
}
|
|
571
574
|
return env;
|
|
@@ -834,11 +837,6 @@ function validateProjectId(projectId) {
|
|
|
834
837
|
function validateBranchName(branchName) {
|
|
835
838
|
(0, import_managed_paths.validateManagedBranchName)(branchName);
|
|
836
839
|
}
|
|
837
|
-
function validatePlanId(planId) {
|
|
838
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
|
|
839
|
-
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
840
|
const NON_RECURSIVE_GIT_CONFIG_ARGS = [
|
|
843
841
|
"-c",
|
|
844
842
|
"submodule.recurse=false",
|
|
@@ -1745,6 +1743,7 @@ const workerGitSecurityTestHarness = {
|
|
|
1745
1743
|
workspaceSyncFailureHydrationIsSafe,
|
|
1746
1744
|
assertWorkerChildAdmission,
|
|
1747
1745
|
executeWriteFileOperation,
|
|
1746
|
+
executeOperation,
|
|
1748
1747
|
executeEditFileOperation,
|
|
1749
1748
|
terminateCredentialBearingChildren,
|
|
1750
1749
|
terminateCredentialBearingChildrenWithRetention,
|
|
@@ -2183,6 +2182,55 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
|
2183
2182
|
edits: edits.length
|
|
2184
2183
|
};
|
|
2185
2184
|
}
|
|
2185
|
+
async function executePlanOperation(input) {
|
|
2186
|
+
const { message, resolvedTarget } = input;
|
|
2187
|
+
const filePath = message.type === "lint_plans" ? (0, import_plan_paths.getWorkerPlansPath)(resolvedTarget.target) : (0, import_plan_paths.getWorkerPlanPath)(resolvedTarget.target, message.planId);
|
|
2188
|
+
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
2189
|
+
target: resolvedTarget.target,
|
|
2190
|
+
filePath,
|
|
2191
|
+
baseUrl: input.baseUrl,
|
|
2192
|
+
token: input.token,
|
|
2193
|
+
rootDir: input.rootDir,
|
|
2194
|
+
sessionId: message.sessionId,
|
|
2195
|
+
access: message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing ? "write" : "read"
|
|
2196
|
+
});
|
|
2197
|
+
const resolved = resolveWorkerFilePath(resolvedTarget.rootPath, filePath, builtInPaths);
|
|
2198
|
+
if (message.type === "lint_plans") {
|
|
2199
|
+
const result = await (0, import_plan_lint.lintPlans)(resolved.absolutePath, message.glob);
|
|
2200
|
+
return {
|
|
2201
|
+
type: "lint_plans",
|
|
2202
|
+
content: result.lintedPlanCount === 0 ? message.glob ? `No plan files matched glob "${message.glob}".` : "No plan files found." : (0, import_plan_lint.formatPlanLintErrors)(result.errors) || "No plan lint issues found."
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
return withFileMutationQueue(mutationQueueKey(resolvedTarget.target, resolved), async () => {
|
|
2206
|
+
input.assertAdmission();
|
|
2207
|
+
if (message.type === "read_plan") {
|
|
2208
|
+
let created = false;
|
|
2209
|
+
if (message.createIfMissing) {
|
|
2210
|
+
try {
|
|
2211
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, (0, import_plan_parser.createPlanMarkdown)(message.planId), { encoding: "utf8", flag: "wx" });
|
|
2212
|
+
created = true;
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
if (error.code !== "EEXIST") throw error;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return {
|
|
2218
|
+
type: "read_plan",
|
|
2219
|
+
created,
|
|
2220
|
+
read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
|
|
2224
|
+
writeWorkerTextFile(resolvedTarget.rootPath, filePath, (0, import_plan_parser.updateTaskCheckbox)(content, message.taskId, message.checked), builtInPaths);
|
|
2225
|
+
return {
|
|
2226
|
+
type: "update_task_status",
|
|
2227
|
+
planId: message.planId,
|
|
2228
|
+
taskId: message.taskId,
|
|
2229
|
+
checked: message.checked,
|
|
2230
|
+
message: `Task ${message.taskId} marked ${message.checked ? "complete" : "incomplete"}.`
|
|
2231
|
+
};
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2186
2234
|
async function executeReadFileOperation(input) {
|
|
2187
2235
|
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
2188
2236
|
target: input.resolvedTarget.target,
|
|
@@ -2561,6 +2609,10 @@ async function executeViewFileBytesOperation(input) {
|
|
|
2561
2609
|
}
|
|
2562
2610
|
async function executeOperation(input) {
|
|
2563
2611
|
switch (input.message.type) {
|
|
2612
|
+
case "read_plan":
|
|
2613
|
+
case "lint_plans":
|
|
2614
|
+
case "update_task_status":
|
|
2615
|
+
return executePlanOperation({ ...input, message: input.message });
|
|
2564
2616
|
case "read":
|
|
2565
2617
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
2566
2618
|
case "write":
|
|
@@ -3420,7 +3472,12 @@ function websocketUrl(baseUrl, label) {
|
|
|
3420
3472
|
url.searchParams.set("version", getWorkerVersion());
|
|
3421
3473
|
return url.toString();
|
|
3422
3474
|
}
|
|
3423
|
-
async function startWorker(options
|
|
3475
|
+
async function startWorker(options, projectRuntime = {
|
|
3476
|
+
projectConfigById: /* @__PURE__ */ new Map(),
|
|
3477
|
+
readyProjectIds: /* @__PURE__ */ new Set(),
|
|
3478
|
+
reconciledProjectConfigFingerprints: /* @__PURE__ */ new Map(),
|
|
3479
|
+
gitIdentity: null
|
|
3480
|
+
}) {
|
|
3424
3481
|
advanceWorkerAdmissionGeneration();
|
|
3425
3482
|
const config = readConfig(options.configPath);
|
|
3426
3483
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
@@ -3461,10 +3518,8 @@ async function startWorker(options) {
|
|
|
3461
3518
|
});
|
|
3462
3519
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3463
3520
|
let projectWorkspaceState = initializedWorkspaceState.state;
|
|
3464
|
-
const projectConfigById
|
|
3521
|
+
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3465
3522
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3466
|
-
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
3467
|
-
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
3468
3523
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
3469
3524
|
const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3470
3525
|
const advertisedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
|
|
@@ -3494,7 +3549,7 @@ async function startWorker(options) {
|
|
|
3494
3549
|
let workspaceRemoteUrl = null;
|
|
3495
3550
|
let workspaceCredentialHelper = null;
|
|
3496
3551
|
let workspaceCredentialUsername = null;
|
|
3497
|
-
let workspaceGitIdentity =
|
|
3552
|
+
let workspaceGitIdentity = projectRuntime.gitIdentity;
|
|
3498
3553
|
let workspaceConfigured = false;
|
|
3499
3554
|
let activeWorkspaceIncidentId = null;
|
|
3500
3555
|
let deferredWorkspaceConfiguration = null;
|
|
@@ -3855,7 +3910,9 @@ async function startWorker(options) {
|
|
|
3855
3910
|
const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
|
|
3856
3911
|
const logMissingCheckouts = (inventory) => {
|
|
3857
3912
|
if (inventory.missingCheckouts.length === 0) return;
|
|
3858
|
-
const described = inventory.missingCheckouts.map(
|
|
3913
|
+
const described = inventory.missingCheckouts.map(
|
|
3914
|
+
({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`
|
|
3915
|
+
).join(", ");
|
|
3859
3916
|
process.stderr.write(
|
|
3860
3917
|
`[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
|
|
3861
3918
|
`
|
|
@@ -4426,7 +4483,9 @@ async function startWorker(options) {
|
|
|
4426
4483
|
error: error instanceof Error ? error.message : String(error)
|
|
4427
4484
|
};
|
|
4428
4485
|
};
|
|
4429
|
-
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4486
|
+
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4487
|
+
(message) => sendWorkerMessageFromCurrentSource(ws, message)
|
|
4488
|
+
);
|
|
4430
4489
|
const performWorkspaceSync = async (input) => {
|
|
4431
4490
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4432
4491
|
throw new Error("Worker workspace configuration has not been received");
|
|
@@ -4483,7 +4542,11 @@ async function startWorker(options) {
|
|
|
4483
4542
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4484
4543
|
const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
|
|
4485
4544
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4486
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({
|
|
4545
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({
|
|
4546
|
+
allowNonFastForward: false,
|
|
4547
|
+
failClosed: false,
|
|
4548
|
+
projects: checkoutInventory.projects
|
|
4549
|
+
});
|
|
4487
4550
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4488
4551
|
for (const bundle of observedProjectHeads) {
|
|
4489
4552
|
for (const observation of bundle.observations) {
|
|
@@ -4672,7 +4735,11 @@ async function startWorker(options) {
|
|
|
4672
4735
|
};
|
|
4673
4736
|
}
|
|
4674
4737
|
};
|
|
4675
|
-
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4738
|
+
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4739
|
+
"remediation",
|
|
4740
|
+
"remediation_confirm",
|
|
4741
|
+
"remediation_reset"
|
|
4742
|
+
]);
|
|
4676
4743
|
const isOrdinaryWorkspaceSyncRequest = (input) => input.confirmedLargeDiff !== true && input.resetToCanonical !== true && !(input.requiredAncestorHeads && input.requiredAncestorHeads.length > 0) && !REMEDIATION_TRIGGER_TYPES.has(input.trigger.type);
|
|
4677
4744
|
const LOCAL_ACTIVITY_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4678
4745
|
"write",
|
|
@@ -4713,7 +4780,14 @@ async function startWorker(options) {
|
|
|
4713
4780
|
trigger: leader.trigger,
|
|
4714
4781
|
assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, leader.trigger)
|
|
4715
4782
|
});
|
|
4716
|
-
return {
|
|
4783
|
+
return {
|
|
4784
|
+
result,
|
|
4785
|
+
ranCycle: true,
|
|
4786
|
+
startedAtMs,
|
|
4787
|
+
finishedAtMs: Date.now(),
|
|
4788
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4789
|
+
staleAttemptIds
|
|
4790
|
+
};
|
|
4717
4791
|
}
|
|
4718
4792
|
});
|
|
4719
4793
|
const runWorkspaceSync = async (input) => {
|
|
@@ -5145,6 +5219,7 @@ async function startWorker(options) {
|
|
|
5145
5219
|
bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
5146
5220
|
visibleGitIdentity = message.gitIdentity;
|
|
5147
5221
|
workspaceGitIdentity = message.gitIdentity;
|
|
5222
|
+
projectRuntime.gitIdentity = message.gitIdentity;
|
|
5148
5223
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
5149
5224
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
5150
5225
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
@@ -6285,7 +6360,7 @@ async function startWorker(options) {
|
|
|
6285
6360
|
});
|
|
6286
6361
|
return;
|
|
6287
6362
|
}
|
|
6288
|
-
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
6363
|
+
if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
6289
6364
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6290
6365
|
sendWorkerMessage(ws, {
|
|
6291
6366
|
type: "operation_result",
|
|
@@ -6295,7 +6370,7 @@ async function startWorker(options) {
|
|
|
6295
6370
|
return;
|
|
6296
6371
|
}
|
|
6297
6372
|
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
6298
|
-
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
|
|
6373
|
+
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing) && targetMayMutateVisibleWorkspace(message.target);
|
|
6299
6374
|
if (reservesVisibleWorkspace) {
|
|
6300
6375
|
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
6301
6376
|
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
@@ -6322,9 +6397,9 @@ async function startWorker(options) {
|
|
|
6322
6397
|
result
|
|
6323
6398
|
})
|
|
6324
6399
|
);
|
|
6325
|
-
if (mutatesVisibleWorkspace) {
|
|
6400
|
+
if (mutatesVisibleWorkspace && (result.type !== "read_plan" || result.created)) {
|
|
6326
6401
|
dirtyTrigger = {
|
|
6327
|
-
type: message.type,
|
|
6402
|
+
type: message.type === "edit" || message.type === "update_task_status" ? "edit" : "write",
|
|
6328
6403
|
sessionId: message.sessionId,
|
|
6329
6404
|
toolCallId: message.requestId,
|
|
6330
6405
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
@@ -6410,7 +6485,7 @@ async function startWorker(options) {
|
|
|
6410
6485
|
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6411
6486
|
`);
|
|
6412
6487
|
setTimeout(() => {
|
|
6413
|
-
void startWorker(options).catch((error) => {
|
|
6488
|
+
void startWorker(options, projectRuntime).catch((error) => {
|
|
6414
6489
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6415
6490
|
`);
|
|
6416
6491
|
process.exit(error instanceof WorkerServerUnavailableError ? import_supervisor.WORKER_RECONNECT_EXIT_CODE : 1);
|
|
@@ -6429,7 +6504,7 @@ async function startWorker(options) {
|
|
|
6429
6504
|
setTimeout(reconnect2, reconnectDelayMs);
|
|
6430
6505
|
};
|
|
6431
6506
|
const reconnect = () => {
|
|
6432
|
-
void startWorker(options).catch((error) => {
|
|
6507
|
+
void startWorker(options, projectRuntime).catch((error) => {
|
|
6433
6508
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6434
6509
|
`);
|
|
6435
6510
|
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var plan_lint_exports = {};
|
|
30
|
+
__export(plan_lint_exports, {
|
|
31
|
+
formatPlanLintErrors: () => formatPlanLintErrors,
|
|
32
|
+
lintPlanContent: () => lintPlanContent,
|
|
33
|
+
lintPlans: () => lintPlans
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(plan_lint_exports);
|
|
36
|
+
var import_promises = __toESM(require("node:fs/promises"), 1);
|
|
37
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
38
|
+
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
39
|
+
var import_plan_parser = require("./plan-parser.cjs");
|
|
40
|
+
var import_plan_paths = require("./plan-paths.cjs");
|
|
41
|
+
var import_plan_parser2 = require("./plan-parser.cjs");
|
|
42
|
+
const LEGACY_SCHEMA_FIELD_REGEX = /^\s*(type|status|parent|phase|discovered_problem)\s*:/m;
|
|
43
|
+
function lintPlanContent(planId, content) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
if (/^---(?:\r?\n|$)/.test(content)) {
|
|
47
|
+
errors.push({
|
|
48
|
+
planId,
|
|
49
|
+
severity: "error",
|
|
50
|
+
message: "YAML frontmatter is not supported. Use plain Markdown with a ## Tasks checklist.",
|
|
51
|
+
line: 1
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (LEGACY_SCHEMA_FIELD_REGEX.test(content)) {
|
|
55
|
+
errors.push({
|
|
56
|
+
planId,
|
|
57
|
+
severity: "error",
|
|
58
|
+
message: "Legacy plan schema fields are not supported. Remove type/status/parent/phase/discovered_problem metadata."
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const titleLineIndex = lines.findIndex((line) => /^# .+\S\s*$/.test(line));
|
|
62
|
+
if (titleLineIndex === -1) {
|
|
63
|
+
errors.push({
|
|
64
|
+
planId,
|
|
65
|
+
severity: "error",
|
|
66
|
+
message: "Plan must start with or contain an H1 title, for example '# Implementation Plan'."
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const range = (0, import_plan_parser.getTasksSectionRange)(content);
|
|
70
|
+
if (!range) {
|
|
71
|
+
errors.push({
|
|
72
|
+
planId,
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: "Plan is missing a ## Tasks section."
|
|
75
|
+
});
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
79
|
+
let taskCount = 0;
|
|
80
|
+
for (let index = range.startIndex + 1; index < range.endIndex; index += 1) {
|
|
81
|
+
const line = lines[index] ?? "";
|
|
82
|
+
if (line.trim() === "") continue;
|
|
83
|
+
const match = line.match(import_plan_parser.PLAN_TASK_LINE_REGEX);
|
|
84
|
+
if (!match || !match[3].trim()) {
|
|
85
|
+
errors.push({
|
|
86
|
+
planId,
|
|
87
|
+
severity: "error",
|
|
88
|
+
line: index + 1,
|
|
89
|
+
message: 'Malformed task line. Use "- [ ] task-id: Description" or "- [x] task-id: Description".'
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
taskCount += 1;
|
|
94
|
+
const taskId = match[2];
|
|
95
|
+
if (taskIds.has(taskId)) {
|
|
96
|
+
errors.push({
|
|
97
|
+
planId,
|
|
98
|
+
severity: "error",
|
|
99
|
+
line: index + 1,
|
|
100
|
+
message: `Duplicate task id "${taskId}".`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
taskIds.add(taskId);
|
|
104
|
+
}
|
|
105
|
+
if (taskCount === 0) {
|
|
106
|
+
errors.push({
|
|
107
|
+
planId,
|
|
108
|
+
severity: "error",
|
|
109
|
+
message: "Plan must include at least one task in the ## Tasks section.",
|
|
110
|
+
line: range.startIndex + 1
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return errors;
|
|
114
|
+
}
|
|
115
|
+
async function lintPlans(plansDir, glob) {
|
|
116
|
+
const errors = [];
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = await import_promises.default.readdir(plansDir, { withFileTypes: true });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.code !== "ENOENT") throw error;
|
|
122
|
+
entries = [];
|
|
123
|
+
}
|
|
124
|
+
const matches = glob ? (0, import_picomatch.default)(glob) : null;
|
|
125
|
+
let lintedPlanCount = 0;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isFile() || !entry.name.endsWith(import_plan_paths.PLAN_EXTENSION)) continue;
|
|
128
|
+
const planId = entry.name.slice(0, -import_plan_paths.PLAN_EXTENSION.length);
|
|
129
|
+
if (!import_plan_parser2.PLAN_TASK_ID_REGEX.test(planId) || matches && !matches(entry.name) && !matches(planId)) continue;
|
|
130
|
+
const content = await import_promises.default.readFile(import_node_path.default.join(plansDir, entry.name), "utf8");
|
|
131
|
+
lintedPlanCount += 1;
|
|
132
|
+
errors.push(...lintPlanContent(planId, content));
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
type: "plan_lint",
|
|
136
|
+
errors,
|
|
137
|
+
errorCount: errors.filter((error) => error.severity === "error").length,
|
|
138
|
+
warningCount: errors.filter((error) => error.severity === "warning").length,
|
|
139
|
+
lintedPlanCount
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function formatPlanLintErrors(errors) {
|
|
143
|
+
if (errors.length === 0) return "";
|
|
144
|
+
const lines = ["## Plan Lint Errors", ""];
|
|
145
|
+
const byPlan = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const error of errors) {
|
|
147
|
+
byPlan.set(error.planId, [...byPlan.get(error.planId) ?? [], error]);
|
|
148
|
+
}
|
|
149
|
+
for (const [planId, planErrors] of byPlan) {
|
|
150
|
+
lines.push(`### ${planId}.plan.md`);
|
|
151
|
+
for (const error of planErrors) {
|
|
152
|
+
const location = error.line ? ` line ${error.line}` : "";
|
|
153
|
+
lines.push(`- ${error.severity.toUpperCase()}${location}: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
lines.push("");
|
|
156
|
+
}
|
|
157
|
+
lines.push("Fix these issues by editing the Markdown plan file.");
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
161
|
+
0 && (module.exports = {
|
|
162
|
+
formatPlanLintErrors,
|
|
163
|
+
lintPlanContent,
|
|
164
|
+
lintPlans
|
|
165
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var plan_parser_exports = {};
|
|
20
|
+
__export(plan_parser_exports, {
|
|
21
|
+
PLAN_TASKS_HEADING_REGEX: () => PLAN_TASKS_HEADING_REGEX,
|
|
22
|
+
PLAN_TASK_ID_REGEX: () => PLAN_TASK_ID_REGEX,
|
|
23
|
+
PLAN_TASK_LINE_REGEX: () => PLAN_TASK_LINE_REGEX,
|
|
24
|
+
createPlanMarkdown: () => createPlanMarkdown,
|
|
25
|
+
formatPlanTitle: () => formatPlanTitle,
|
|
26
|
+
getPlanMetadata: () => getPlanMetadata,
|
|
27
|
+
getTasksSectionRange: () => getTasksSectionRange,
|
|
28
|
+
parseMarkdownPlan: () => parseMarkdownPlan,
|
|
29
|
+
parsePlan: () => parsePlan,
|
|
30
|
+
updateTaskCheckbox: () => updateTaskCheckbox
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(plan_parser_exports);
|
|
33
|
+
const PLAN_TASK_ID_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
34
|
+
const PLAN_TASK_LINE_REGEX = /^- \[( |x)\] ([a-z0-9][a-z0-9-]*): (.+)$/;
|
|
35
|
+
const PLAN_TASKS_HEADING_REGEX = /^## Tasks\s*$/;
|
|
36
|
+
function getPlanMetadata(content) {
|
|
37
|
+
const title = content.split(/\r?\n/).map((line) => line.match(/^# (.+)$/)?.[1]?.trim()).find((value) => Boolean(value));
|
|
38
|
+
return { title: title || "Untitled Plan" };
|
|
39
|
+
}
|
|
40
|
+
function getTasksSectionRange(content) {
|
|
41
|
+
const lines = content.split(/\r?\n/);
|
|
42
|
+
const startIndex = lines.findIndex((line) => PLAN_TASKS_HEADING_REGEX.test(line));
|
|
43
|
+
if (startIndex === -1) return null;
|
|
44
|
+
let endIndex = lines.length;
|
|
45
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
46
|
+
if (/^##\s+/.test(lines[index] ?? "")) {
|
|
47
|
+
endIndex = index;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { startIndex, endIndex };
|
|
52
|
+
}
|
|
53
|
+
function parseMarkdownPlan(content, planId) {
|
|
54
|
+
const lines = content.split(/\r?\n/);
|
|
55
|
+
const { title } = getPlanMetadata(content);
|
|
56
|
+
const range = getTasksSectionRange(content);
|
|
57
|
+
const tasks = [];
|
|
58
|
+
if (range) {
|
|
59
|
+
for (let index = range.startIndex + 1; index < range.endIndex; index += 1) {
|
|
60
|
+
const line = lines[index] ?? "";
|
|
61
|
+
const match = line.match(PLAN_TASK_LINE_REGEX);
|
|
62
|
+
if (!match) continue;
|
|
63
|
+
const description = match[3].trim();
|
|
64
|
+
if (!description) continue;
|
|
65
|
+
tasks.push({
|
|
66
|
+
id: match[2],
|
|
67
|
+
description,
|
|
68
|
+
checked: match[1] === "x",
|
|
69
|
+
lineNumber: index + 1
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const currentTask = tasks.find((task) => !task.checked) ?? null;
|
|
74
|
+
return {
|
|
75
|
+
id: planId,
|
|
76
|
+
title,
|
|
77
|
+
content,
|
|
78
|
+
tasks,
|
|
79
|
+
currentTask,
|
|
80
|
+
complete: tasks.length > 0 && tasks.every((task) => task.checked),
|
|
81
|
+
tasksStartLine: range ? range.startIndex + 1 : null,
|
|
82
|
+
tasksEndLine: range ? range.endIndex : null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const parsePlan = parseMarkdownPlan;
|
|
86
|
+
function createPlanMarkdown(planId, title = formatPlanTitle(planId)) {
|
|
87
|
+
return `# ${title}
|
|
88
|
+
|
|
89
|
+
Brief implementation notes.
|
|
90
|
+
|
|
91
|
+
## Tasks
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
function updateTaskCheckbox(content, taskId, checked) {
|
|
95
|
+
if (!PLAN_TASK_ID_REGEX.test(taskId)) {
|
|
96
|
+
throw new Error(`Invalid task id: ${taskId}`);
|
|
97
|
+
}
|
|
98
|
+
const lines = content.split(/\r?\n/);
|
|
99
|
+
const range = getTasksSectionRange(content);
|
|
100
|
+
if (!range) {
|
|
101
|
+
throw new Error("Plan is missing a ## Tasks section.");
|
|
102
|
+
}
|
|
103
|
+
for (let index = range.startIndex + 1; index < range.endIndex; index += 1) {
|
|
104
|
+
const line = lines[index] ?? "";
|
|
105
|
+
const match = line.match(PLAN_TASK_LINE_REGEX);
|
|
106
|
+
if (!match || match[2] !== taskId) continue;
|
|
107
|
+
lines[index] = `- [${checked ? "x" : " "}] ${match[2]}: ${match[3].trim()}`;
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Task not found in plan: ${taskId}`);
|
|
111
|
+
}
|
|
112
|
+
function formatPlanTitle(planId) {
|
|
113
|
+
return planId.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
114
|
+
}
|
|
115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
116
|
+
0 && (module.exports = {
|
|
117
|
+
PLAN_TASKS_HEADING_REGEX,
|
|
118
|
+
PLAN_TASK_ID_REGEX,
|
|
119
|
+
PLAN_TASK_LINE_REGEX,
|
|
120
|
+
createPlanMarkdown,
|
|
121
|
+
formatPlanTitle,
|
|
122
|
+
getPlanMetadata,
|
|
123
|
+
getTasksSectionRange,
|
|
124
|
+
parseMarkdownPlan,
|
|
125
|
+
parsePlan,
|
|
126
|
+
updateTaskCheckbox
|
|
127
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var plan_paths_exports = {};
|
|
20
|
+
__export(plan_paths_exports, {
|
|
21
|
+
PLAN_EXTENSION: () => PLAN_EXTENSION,
|
|
22
|
+
assertPlanTarget: () => assertPlanTarget,
|
|
23
|
+
getWorkerPlanPath: () => getWorkerPlanPath,
|
|
24
|
+
getWorkerPlansPath: () => getWorkerPlansPath,
|
|
25
|
+
validatePlanId: () => validatePlanId
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(plan_paths_exports);
|
|
28
|
+
var import_plan_parser = require("./plan-parser.cjs");
|
|
29
|
+
const PLAN_EXTENSION = ".plan.md";
|
|
30
|
+
function validatePlanId(planId) {
|
|
31
|
+
if (!import_plan_parser.PLAN_TASK_ID_REGEX.test(planId)) throw new Error(`Invalid plan id: ${planId}`);
|
|
32
|
+
}
|
|
33
|
+
function assertPlanTarget(target) {
|
|
34
|
+
if (target.type === "workspace" && target.rootProfile !== "visible_projects") {
|
|
35
|
+
throw new Error("Ordinary plans are unavailable in canonical synchronization sessions");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function getWorkerPlansPath(target) {
|
|
39
|
+
assertPlanTarget(target);
|
|
40
|
+
return target.type === "project" ? "$R5D_ROOT/plans/$R5D_PROJECT_ID/$R5D_BRANCH_NAME" : "$R5D_ROOT/plans/workspace";
|
|
41
|
+
}
|
|
42
|
+
function getWorkerPlanPath(target, planId) {
|
|
43
|
+
validatePlanId(planId);
|
|
44
|
+
return `${getWorkerPlansPath(target)}/${planId}${PLAN_EXTENSION}`;
|
|
45
|
+
}
|
|
46
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
47
|
+
0 && (module.exports = {
|
|
48
|
+
PLAN_EXTENSION,
|
|
49
|
+
assertPlanTarget,
|
|
50
|
+
getWorkerPlanPath,
|
|
51
|
+
getWorkerPlansPath,
|
|
52
|
+
validatePlanId
|
|
53
|
+
});
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { getWorkerPlanPath, getWorkerPlansPath, validatePlanId } from "./plan-paths.mjs";
|
|
3
|
+
import { createPlanMarkdown, updateTaskCheckbox } from "./plan-parser.mjs";
|
|
4
|
+
import { lintPlans, formatPlanLintErrors } from "./plan-lint.mjs";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { createHash } from "node:crypto";
|
|
@@ -845,11 +848,6 @@ function validateProjectId(projectId) {
|
|
|
845
848
|
function validateBranchName(branchName) {
|
|
846
849
|
validateManagedBranchName(branchName);
|
|
847
850
|
}
|
|
848
|
-
function validatePlanId(planId) {
|
|
849
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
|
|
850
|
-
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
851
|
const NON_RECURSIVE_GIT_CONFIG_ARGS = [
|
|
854
852
|
"-c",
|
|
855
853
|
"submodule.recurse=false",
|
|
@@ -1756,6 +1754,7 @@ const workerGitSecurityTestHarness = {
|
|
|
1756
1754
|
workspaceSyncFailureHydrationIsSafe,
|
|
1757
1755
|
assertWorkerChildAdmission,
|
|
1758
1756
|
executeWriteFileOperation,
|
|
1757
|
+
executeOperation,
|
|
1759
1758
|
executeEditFileOperation,
|
|
1760
1759
|
terminateCredentialBearingChildren,
|
|
1761
1760
|
terminateCredentialBearingChildrenWithRetention,
|
|
@@ -2194,6 +2193,55 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
|
2194
2193
|
edits: edits.length
|
|
2195
2194
|
};
|
|
2196
2195
|
}
|
|
2196
|
+
async function executePlanOperation(input) {
|
|
2197
|
+
const { message, resolvedTarget } = input;
|
|
2198
|
+
const filePath = message.type === "lint_plans" ? getWorkerPlansPath(resolvedTarget.target) : getWorkerPlanPath(resolvedTarget.target, message.planId);
|
|
2199
|
+
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
2200
|
+
target: resolvedTarget.target,
|
|
2201
|
+
filePath,
|
|
2202
|
+
baseUrl: input.baseUrl,
|
|
2203
|
+
token: input.token,
|
|
2204
|
+
rootDir: input.rootDir,
|
|
2205
|
+
sessionId: message.sessionId,
|
|
2206
|
+
access: message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing ? "write" : "read"
|
|
2207
|
+
});
|
|
2208
|
+
const resolved = resolveWorkerFilePath(resolvedTarget.rootPath, filePath, builtInPaths);
|
|
2209
|
+
if (message.type === "lint_plans") {
|
|
2210
|
+
const result = await lintPlans(resolved.absolutePath, message.glob);
|
|
2211
|
+
return {
|
|
2212
|
+
type: "lint_plans",
|
|
2213
|
+
content: result.lintedPlanCount === 0 ? message.glob ? `No plan files matched glob "${message.glob}".` : "No plan files found." : formatPlanLintErrors(result.errors) || "No plan lint issues found."
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
return withFileMutationQueue(mutationQueueKey(resolvedTarget.target, resolved), async () => {
|
|
2217
|
+
input.assertAdmission();
|
|
2218
|
+
if (message.type === "read_plan") {
|
|
2219
|
+
let created = false;
|
|
2220
|
+
if (message.createIfMissing) {
|
|
2221
|
+
try {
|
|
2222
|
+
fs.writeFileSync(resolved.absolutePath, createPlanMarkdown(message.planId), { encoding: "utf8", flag: "wx" });
|
|
2223
|
+
created = true;
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
if (error.code !== "EEXIST") throw error;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return {
|
|
2229
|
+
type: "read_plan",
|
|
2230
|
+
created,
|
|
2231
|
+
read: readWorkerTextFile(resolvedTarget.rootPath, filePath, void 0, void 0, builtInPaths)
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
const content = fs.readFileSync(resolved.absolutePath, "utf8");
|
|
2235
|
+
writeWorkerTextFile(resolvedTarget.rootPath, filePath, updateTaskCheckbox(content, message.taskId, message.checked), builtInPaths);
|
|
2236
|
+
return {
|
|
2237
|
+
type: "update_task_status",
|
|
2238
|
+
planId: message.planId,
|
|
2239
|
+
taskId: message.taskId,
|
|
2240
|
+
checked: message.checked,
|
|
2241
|
+
message: `Task ${message.taskId} marked ${message.checked ? "complete" : "incomplete"}.`
|
|
2242
|
+
};
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2197
2245
|
async function executeReadFileOperation(input) {
|
|
2198
2246
|
const builtInPaths = await prepareBuiltInToolPathsForTarget({
|
|
2199
2247
|
target: input.resolvedTarget.target,
|
|
@@ -2572,6 +2620,10 @@ async function executeViewFileBytesOperation(input) {
|
|
|
2572
2620
|
}
|
|
2573
2621
|
async function executeOperation(input) {
|
|
2574
2622
|
switch (input.message.type) {
|
|
2623
|
+
case "read_plan":
|
|
2624
|
+
case "lint_plans":
|
|
2625
|
+
case "update_task_status":
|
|
2626
|
+
return executePlanOperation({ ...input, message: input.message });
|
|
2575
2627
|
case "read":
|
|
2576
2628
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
2577
2629
|
case "write":
|
|
@@ -3431,7 +3483,12 @@ function websocketUrl(baseUrl, label) {
|
|
|
3431
3483
|
url.searchParams.set("version", getWorkerVersion());
|
|
3432
3484
|
return url.toString();
|
|
3433
3485
|
}
|
|
3434
|
-
async function startWorker(options
|
|
3486
|
+
async function startWorker(options, projectRuntime = {
|
|
3487
|
+
projectConfigById: /* @__PURE__ */ new Map(),
|
|
3488
|
+
readyProjectIds: /* @__PURE__ */ new Set(),
|
|
3489
|
+
reconciledProjectConfigFingerprints: /* @__PURE__ */ new Map(),
|
|
3490
|
+
gitIdentity: null
|
|
3491
|
+
}) {
|
|
3435
3492
|
advanceWorkerAdmissionGeneration();
|
|
3436
3493
|
const config = readConfig(options.configPath);
|
|
3437
3494
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
@@ -3472,10 +3529,8 @@ async function startWorker(options) {
|
|
|
3472
3529
|
});
|
|
3473
3530
|
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3474
3531
|
let projectWorkspaceState = initializedWorkspaceState.state;
|
|
3475
|
-
const projectConfigById
|
|
3532
|
+
const { projectConfigById, readyProjectIds, reconciledProjectConfigFingerprints } = projectRuntime;
|
|
3476
3533
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
3477
|
-
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
3478
|
-
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
3479
3534
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
3480
3535
|
const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3481
3536
|
const advertisedProjectMirrorMountTokens = /* @__PURE__ */ new Map();
|
|
@@ -3505,7 +3560,7 @@ async function startWorker(options) {
|
|
|
3505
3560
|
let workspaceRemoteUrl = null;
|
|
3506
3561
|
let workspaceCredentialHelper = null;
|
|
3507
3562
|
let workspaceCredentialUsername = null;
|
|
3508
|
-
let workspaceGitIdentity =
|
|
3563
|
+
let workspaceGitIdentity = projectRuntime.gitIdentity;
|
|
3509
3564
|
let workspaceConfigured = false;
|
|
3510
3565
|
let activeWorkspaceIncidentId = null;
|
|
3511
3566
|
let deferredWorkspaceConfiguration = null;
|
|
@@ -3866,7 +3921,9 @@ async function startWorker(options) {
|
|
|
3866
3921
|
const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
|
|
3867
3922
|
const logMissingCheckouts = (inventory) => {
|
|
3868
3923
|
if (inventory.missingCheckouts.length === 0) return;
|
|
3869
|
-
const described = inventory.missingCheckouts.map(
|
|
3924
|
+
const described = inventory.missingCheckouts.map(
|
|
3925
|
+
({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`
|
|
3926
|
+
).join(", ");
|
|
3870
3927
|
process.stderr.write(
|
|
3871
3928
|
`[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
|
|
3872
3929
|
`
|
|
@@ -4437,7 +4494,9 @@ async function startWorker(options) {
|
|
|
4437
4494
|
error: error instanceof Error ? error.message : String(error)
|
|
4438
4495
|
};
|
|
4439
4496
|
};
|
|
4440
|
-
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4497
|
+
const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks(
|
|
4498
|
+
(message) => sendWorkerMessageFromCurrentSource(ws, message)
|
|
4499
|
+
);
|
|
4441
4500
|
const performWorkspaceSync = async (input) => {
|
|
4442
4501
|
if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
|
|
4443
4502
|
throw new Error("Worker workspace configuration has not been received");
|
|
@@ -4494,7 +4553,11 @@ async function startWorker(options) {
|
|
|
4494
4553
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
4495
4554
|
const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
|
|
4496
4555
|
if (outerRemediation) workspaceProjectionLedger.reset();
|
|
4497
|
-
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({
|
|
4556
|
+
const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({
|
|
4557
|
+
allowNonFastForward: false,
|
|
4558
|
+
failClosed: false,
|
|
4559
|
+
projects: checkoutInventory.projects
|
|
4560
|
+
});
|
|
4498
4561
|
const expectedMirrorHeads = /* @__PURE__ */ new Map();
|
|
4499
4562
|
for (const bundle of observedProjectHeads) {
|
|
4500
4563
|
for (const observation of bundle.observations) {
|
|
@@ -4683,7 +4746,11 @@ async function startWorker(options) {
|
|
|
4683
4746
|
};
|
|
4684
4747
|
}
|
|
4685
4748
|
};
|
|
4686
|
-
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4749
|
+
const REMEDIATION_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4750
|
+
"remediation",
|
|
4751
|
+
"remediation_confirm",
|
|
4752
|
+
"remediation_reset"
|
|
4753
|
+
]);
|
|
4687
4754
|
const isOrdinaryWorkspaceSyncRequest = (input) => input.confirmedLargeDiff !== true && input.resetToCanonical !== true && !(input.requiredAncestorHeads && input.requiredAncestorHeads.length > 0) && !REMEDIATION_TRIGGER_TYPES.has(input.trigger.type);
|
|
4688
4755
|
const LOCAL_ACTIVITY_TRIGGER_TYPES = /* @__PURE__ */ new Set([
|
|
4689
4756
|
"write",
|
|
@@ -4724,7 +4791,14 @@ async function startWorker(options) {
|
|
|
4724
4791
|
trigger: leader.trigger,
|
|
4725
4792
|
assertStillAdmitted: () => assertWorkspaceSyncStillAdmitted(admission, leader.trigger)
|
|
4726
4793
|
});
|
|
4727
|
-
return {
|
|
4794
|
+
return {
|
|
4795
|
+
result,
|
|
4796
|
+
ranCycle: true,
|
|
4797
|
+
startedAtMs,
|
|
4798
|
+
finishedAtMs: Date.now(),
|
|
4799
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4800
|
+
staleAttemptIds
|
|
4801
|
+
};
|
|
4728
4802
|
}
|
|
4729
4803
|
});
|
|
4730
4804
|
const runWorkspaceSync = async (input) => {
|
|
@@ -5156,6 +5230,7 @@ async function startWorker(options) {
|
|
|
5156
5230
|
bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
5157
5231
|
visibleGitIdentity = message.gitIdentity;
|
|
5158
5232
|
workspaceGitIdentity = message.gitIdentity;
|
|
5233
|
+
projectRuntime.gitIdentity = message.gitIdentity;
|
|
5159
5234
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
5160
5235
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
5161
5236
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
@@ -6296,7 +6371,7 @@ async function startWorker(options) {
|
|
|
6296
6371
|
});
|
|
6297
6372
|
return;
|
|
6298
6373
|
}
|
|
6299
|
-
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
6374
|
+
if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
6300
6375
|
if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
|
|
6301
6376
|
sendWorkerMessage(ws, {
|
|
6302
6377
|
type: "operation_result",
|
|
@@ -6306,7 +6381,7 @@ async function startWorker(options) {
|
|
|
6306
6381
|
return;
|
|
6307
6382
|
}
|
|
6308
6383
|
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
6309
|
-
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
|
|
6384
|
+
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit" || message.type === "update_task_status" || message.type === "read_plan" && message.createIfMissing) && targetMayMutateVisibleWorkspace(message.target);
|
|
6310
6385
|
if (reservesVisibleWorkspace) {
|
|
6311
6386
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
6312
6387
|
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
@@ -6333,9 +6408,9 @@ async function startWorker(options) {
|
|
|
6333
6408
|
result
|
|
6334
6409
|
})
|
|
6335
6410
|
);
|
|
6336
|
-
if (mutatesVisibleWorkspace) {
|
|
6411
|
+
if (mutatesVisibleWorkspace && (result.type !== "read_plan" || result.created)) {
|
|
6337
6412
|
dirtyTrigger = {
|
|
6338
|
-
type: message.type,
|
|
6413
|
+
type: message.type === "edit" || message.type === "update_task_status" ? "edit" : "write",
|
|
6339
6414
|
sessionId: message.sessionId,
|
|
6340
6415
|
toolCallId: message.requestId,
|
|
6341
6416
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
@@ -6421,7 +6496,7 @@ async function startWorker(options) {
|
|
|
6421
6496
|
process.stderr.write(`[r5d-worker] workspace recovery complete; reconnecting in ${reconnectDelayMs}ms
|
|
6422
6497
|
`);
|
|
6423
6498
|
setTimeout(() => {
|
|
6424
|
-
void startWorker(options).catch((error) => {
|
|
6499
|
+
void startWorker(options, projectRuntime).catch((error) => {
|
|
6425
6500
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6426
6501
|
`);
|
|
6427
6502
|
process.exit(error instanceof WorkerServerUnavailableError ? WORKER_RECONNECT_EXIT_CODE : 1);
|
|
@@ -6440,7 +6515,7 @@ async function startWorker(options) {
|
|
|
6440
6515
|
setTimeout(reconnect2, reconnectDelayMs);
|
|
6441
6516
|
};
|
|
6442
6517
|
const reconnect = () => {
|
|
6443
|
-
void startWorker(options).catch((error) => {
|
|
6518
|
+
void startWorker(options, projectRuntime).catch((error) => {
|
|
6444
6519
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
6445
6520
|
`);
|
|
6446
6521
|
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import picomatch from "picomatch";
|
|
4
|
+
import { getTasksSectionRange, PLAN_TASK_LINE_REGEX } from "./plan-parser.mjs";
|
|
5
|
+
import { PLAN_EXTENSION } from "./plan-paths.mjs";
|
|
6
|
+
import { PLAN_TASK_ID_REGEX } from "./plan-parser.mjs";
|
|
7
|
+
const LEGACY_SCHEMA_FIELD_REGEX = /^\s*(type|status|parent|phase|discovered_problem)\s*:/m;
|
|
8
|
+
function lintPlanContent(planId, content) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
const lines = content.split(/\r?\n/);
|
|
11
|
+
if (/^---(?:\r?\n|$)/.test(content)) {
|
|
12
|
+
errors.push({
|
|
13
|
+
planId,
|
|
14
|
+
severity: "error",
|
|
15
|
+
message: "YAML frontmatter is not supported. Use plain Markdown with a ## Tasks checklist.",
|
|
16
|
+
line: 1
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (LEGACY_SCHEMA_FIELD_REGEX.test(content)) {
|
|
20
|
+
errors.push({
|
|
21
|
+
planId,
|
|
22
|
+
severity: "error",
|
|
23
|
+
message: "Legacy plan schema fields are not supported. Remove type/status/parent/phase/discovered_problem metadata."
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const titleLineIndex = lines.findIndex((line) => /^# .+\S\s*$/.test(line));
|
|
27
|
+
if (titleLineIndex === -1) {
|
|
28
|
+
errors.push({
|
|
29
|
+
planId,
|
|
30
|
+
severity: "error",
|
|
31
|
+
message: "Plan must start with or contain an H1 title, for example '# Implementation Plan'."
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const range = getTasksSectionRange(content);
|
|
35
|
+
if (!range) {
|
|
36
|
+
errors.push({
|
|
37
|
+
planId,
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: "Plan is missing a ## Tasks section."
|
|
40
|
+
});
|
|
41
|
+
return errors;
|
|
42
|
+
}
|
|
43
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
44
|
+
let taskCount = 0;
|
|
45
|
+
for (let index = range.startIndex + 1; index < range.endIndex; index += 1) {
|
|
46
|
+
const line = lines[index] ?? "";
|
|
47
|
+
if (line.trim() === "") continue;
|
|
48
|
+
const match = line.match(PLAN_TASK_LINE_REGEX);
|
|
49
|
+
if (!match || !match[3].trim()) {
|
|
50
|
+
errors.push({
|
|
51
|
+
planId,
|
|
52
|
+
severity: "error",
|
|
53
|
+
line: index + 1,
|
|
54
|
+
message: 'Malformed task line. Use "- [ ] task-id: Description" or "- [x] task-id: Description".'
|
|
55
|
+
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
taskCount += 1;
|
|
59
|
+
const taskId = match[2];
|
|
60
|
+
if (taskIds.has(taskId)) {
|
|
61
|
+
errors.push({
|
|
62
|
+
planId,
|
|
63
|
+
severity: "error",
|
|
64
|
+
line: index + 1,
|
|
65
|
+
message: `Duplicate task id "${taskId}".`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
taskIds.add(taskId);
|
|
69
|
+
}
|
|
70
|
+
if (taskCount === 0) {
|
|
71
|
+
errors.push({
|
|
72
|
+
planId,
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: "Plan must include at least one task in the ## Tasks section.",
|
|
75
|
+
line: range.startIndex + 1
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
async function lintPlans(plansDir, glob) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await fs.readdir(plansDir, { withFileTypes: true });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT") throw error;
|
|
87
|
+
entries = [];
|
|
88
|
+
}
|
|
89
|
+
const matches = glob ? picomatch(glob) : null;
|
|
90
|
+
let lintedPlanCount = 0;
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isFile() || !entry.name.endsWith(PLAN_EXTENSION)) continue;
|
|
93
|
+
const planId = entry.name.slice(0, -PLAN_EXTENSION.length);
|
|
94
|
+
if (!PLAN_TASK_ID_REGEX.test(planId) || matches && !matches(entry.name) && !matches(planId)) continue;
|
|
95
|
+
const content = await fs.readFile(path.join(plansDir, entry.name), "utf8");
|
|
96
|
+
lintedPlanCount += 1;
|
|
97
|
+
errors.push(...lintPlanContent(planId, content));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
type: "plan_lint",
|
|
101
|
+
errors,
|
|
102
|
+
errorCount: errors.filter((error) => error.severity === "error").length,
|
|
103
|
+
warningCount: errors.filter((error) => error.severity === "warning").length,
|
|
104
|
+
lintedPlanCount
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function formatPlanLintErrors(errors) {
|
|
108
|
+
if (errors.length === 0) return "";
|
|
109
|
+
const lines = ["## Plan Lint Errors", ""];
|
|
110
|
+
const byPlan = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const error of errors) {
|
|
112
|
+
byPlan.set(error.planId, [...byPlan.get(error.planId) ?? [], error]);
|
|
113
|
+
}
|
|
114
|
+
for (const [planId, planErrors] of byPlan) {
|
|
115
|
+
lines.push(`### ${planId}.plan.md`);
|
|
116
|
+
for (const error of planErrors) {
|
|
117
|
+
const location = error.line ? ` line ${error.line}` : "";
|
|
118
|
+
lines.push(`- ${error.severity.toUpperCase()}${location}: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
lines.push("Fix these issues by editing the Markdown plan file.");
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
formatPlanLintErrors,
|
|
127
|
+
lintPlanContent,
|
|
128
|
+
lintPlans
|
|
129
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const PLAN_TASK_ID_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
2
|
+
const PLAN_TASK_LINE_REGEX = /^- \[( |x)\] ([a-z0-9][a-z0-9-]*): (.+)$/;
|
|
3
|
+
const PLAN_TASKS_HEADING_REGEX = /^## Tasks\s*$/;
|
|
4
|
+
function getPlanMetadata(content) {
|
|
5
|
+
const title = content.split(/\r?\n/).map((line) => line.match(/^# (.+)$/)?.[1]?.trim()).find((value) => Boolean(value));
|
|
6
|
+
return { title: title || "Untitled Plan" };
|
|
7
|
+
}
|
|
8
|
+
function getTasksSectionRange(content) {
|
|
9
|
+
const lines = content.split(/\r?\n/);
|
|
10
|
+
const startIndex = lines.findIndex((line) => PLAN_TASKS_HEADING_REGEX.test(line));
|
|
11
|
+
if (startIndex === -1) return null;
|
|
12
|
+
let endIndex = lines.length;
|
|
13
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
14
|
+
if (/^##\s+/.test(lines[index] ?? "")) {
|
|
15
|
+
endIndex = index;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { startIndex, endIndex };
|
|
20
|
+
}
|
|
21
|
+
function parseMarkdownPlan(content, planId) {
|
|
22
|
+
const lines = content.split(/\r?\n/);
|
|
23
|
+
const { title } = getPlanMetadata(content);
|
|
24
|
+
const range = getTasksSectionRange(content);
|
|
25
|
+
const tasks = [];
|
|
26
|
+
if (range) {
|
|
27
|
+
for (let index = range.startIndex + 1; index < range.endIndex; index += 1) {
|
|
28
|
+
const line = lines[index] ?? "";
|
|
29
|
+
const match = line.match(PLAN_TASK_LINE_REGEX);
|
|
30
|
+
if (!match) continue;
|
|
31
|
+
const description = match[3].trim();
|
|
32
|
+
if (!description) continue;
|
|
33
|
+
tasks.push({
|
|
34
|
+
id: match[2],
|
|
35
|
+
description,
|
|
36
|
+
checked: match[1] === "x",
|
|
37
|
+
lineNumber: index + 1
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const currentTask = tasks.find((task) => !task.checked) ?? null;
|
|
42
|
+
return {
|
|
43
|
+
id: planId,
|
|
44
|
+
title,
|
|
45
|
+
content,
|
|
46
|
+
tasks,
|
|
47
|
+
currentTask,
|
|
48
|
+
complete: tasks.length > 0 && tasks.every((task) => task.checked),
|
|
49
|
+
tasksStartLine: range ? range.startIndex + 1 : null,
|
|
50
|
+
tasksEndLine: range ? range.endIndex : null
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const parsePlan = parseMarkdownPlan;
|
|
54
|
+
function createPlanMarkdown(planId, title = formatPlanTitle(planId)) {
|
|
55
|
+
return `# ${title}
|
|
56
|
+
|
|
57
|
+
Brief implementation notes.
|
|
58
|
+
|
|
59
|
+
## Tasks
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function updateTaskCheckbox(content, taskId, checked) {
|
|
63
|
+
if (!PLAN_TASK_ID_REGEX.test(taskId)) {
|
|
64
|
+
throw new Error(`Invalid task id: ${taskId}`);
|
|
65
|
+
}
|
|
66
|
+
const lines = content.split(/\r?\n/);
|
|
67
|
+
const range = getTasksSectionRange(content);
|
|
68
|
+
if (!range) {
|
|
69
|
+
throw new Error("Plan is missing a ## Tasks section.");
|
|
70
|
+
}
|
|
71
|
+
for (let index = range.startIndex + 1; index < range.endIndex; index += 1) {
|
|
72
|
+
const line = lines[index] ?? "";
|
|
73
|
+
const match = line.match(PLAN_TASK_LINE_REGEX);
|
|
74
|
+
if (!match || match[2] !== taskId) continue;
|
|
75
|
+
lines[index] = `- [${checked ? "x" : " "}] ${match[2]}: ${match[3].trim()}`;
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Task not found in plan: ${taskId}`);
|
|
79
|
+
}
|
|
80
|
+
function formatPlanTitle(planId) {
|
|
81
|
+
return planId.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
PLAN_TASKS_HEADING_REGEX,
|
|
85
|
+
PLAN_TASK_ID_REGEX,
|
|
86
|
+
PLAN_TASK_LINE_REGEX,
|
|
87
|
+
createPlanMarkdown,
|
|
88
|
+
formatPlanTitle,
|
|
89
|
+
getPlanMetadata,
|
|
90
|
+
getTasksSectionRange,
|
|
91
|
+
parseMarkdownPlan,
|
|
92
|
+
parsePlan,
|
|
93
|
+
updateTaskCheckbox
|
|
94
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PLAN_TASK_ID_REGEX } from "./plan-parser.mjs";
|
|
2
|
+
const PLAN_EXTENSION = ".plan.md";
|
|
3
|
+
function validatePlanId(planId) {
|
|
4
|
+
if (!PLAN_TASK_ID_REGEX.test(planId)) throw new Error(`Invalid plan id: ${planId}`);
|
|
5
|
+
}
|
|
6
|
+
function assertPlanTarget(target) {
|
|
7
|
+
if (target.type === "workspace" && target.rootProfile !== "visible_projects") {
|
|
8
|
+
throw new Error("Ordinary plans are unavailable in canonical synchronization sessions");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function getWorkerPlansPath(target) {
|
|
12
|
+
assertPlanTarget(target);
|
|
13
|
+
return target.type === "project" ? "$R5D_ROOT/plans/$R5D_PROJECT_ID/$R5D_BRANCH_NAME" : "$R5D_ROOT/plans/workspace";
|
|
14
|
+
}
|
|
15
|
+
function getWorkerPlanPath(target, planId) {
|
|
16
|
+
validatePlanId(planId);
|
|
17
|
+
return `${getWorkerPlansPath(target)}/${planId}${PLAN_EXTENSION}`;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
PLAN_EXTENSION,
|
|
21
|
+
assertPlanTarget,
|
|
22
|
+
getWorkerPlanPath,
|
|
23
|
+
getWorkerPlansPath,
|
|
24
|
+
validatePlanId
|
|
25
|
+
};
|
package/dist/types/main.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import type { WorkerLintPlansResult, WorkerUpdatePlanTaskResult } from "./plan-paths";
|
|
2
3
|
import { Database } from "bun:sqlite";
|
|
3
4
|
import type { WorkerGitIdentity } from "./git-identity";
|
|
4
5
|
import { configureGitHubRegistryAuthFiles, type PreparedPrivateAuthFileGeneration } from "./registry-auth";
|
|
@@ -424,6 +425,27 @@ type WorkerServerMessage = {
|
|
|
424
425
|
} | {
|
|
425
426
|
type: "pty_close";
|
|
426
427
|
ptyId: string;
|
|
428
|
+
} | {
|
|
429
|
+
type: "read_plan";
|
|
430
|
+
requestId: string;
|
|
431
|
+
target: WorkerSessionTarget;
|
|
432
|
+
sessionId?: string;
|
|
433
|
+
planId: string;
|
|
434
|
+
createIfMissing: boolean;
|
|
435
|
+
} | {
|
|
436
|
+
type: "lint_plans";
|
|
437
|
+
requestId: string;
|
|
438
|
+
target: WorkerSessionTarget;
|
|
439
|
+
sessionId?: string;
|
|
440
|
+
glob?: string;
|
|
441
|
+
} | {
|
|
442
|
+
type: "update_task_status";
|
|
443
|
+
requestId: string;
|
|
444
|
+
target: WorkerSessionTarget;
|
|
445
|
+
sessionId?: string;
|
|
446
|
+
planId: string;
|
|
447
|
+
taskId: string;
|
|
448
|
+
checked: boolean;
|
|
427
449
|
} | {
|
|
428
450
|
type: "read";
|
|
429
451
|
requestId: string;
|
|
@@ -514,6 +536,11 @@ type WorkerGitHubCredential = {
|
|
|
514
536
|
token: string;
|
|
515
537
|
missingScopes: Array<"repo" | "read:org" | "gist" | "workflow" | "read:packages" | "write:packages">;
|
|
516
538
|
};
|
|
539
|
+
type WorkerReadPlanResult = {
|
|
540
|
+
type: "read_plan";
|
|
541
|
+
read: WorkerReadFileResult;
|
|
542
|
+
created: boolean;
|
|
543
|
+
};
|
|
517
544
|
type WorkerWriteFileResult = {
|
|
518
545
|
type: "write";
|
|
519
546
|
file: string;
|
|
@@ -587,7 +614,10 @@ type WorkerCodeReadResult = {
|
|
|
587
614
|
mtime: string;
|
|
588
615
|
base64: string;
|
|
589
616
|
};
|
|
590
|
-
type WorkerOperationResult = WorkerReadFileResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult;
|
|
617
|
+
type WorkerOperationResult = WorkerReadFileResult | WorkerReadPlanResult | WorkerLintPlansResult | WorkerUpdatePlanTaskResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult;
|
|
618
|
+
type WorkerOperationServerMessage = Extract<WorkerServerMessage, {
|
|
619
|
+
type: "read_plan" | "lint_plans" | "update_task_status" | "read" | "write" | "edit" | "grep" | "find" | "ls" | "view_file_bytes" | "code_list" | "code_read";
|
|
620
|
+
}>;
|
|
591
621
|
declare class WorkspaceSyncAdmissionChangedError extends Error {
|
|
592
622
|
readonly name = "WorkspaceSyncAdmissionChangedError";
|
|
593
623
|
}
|
|
@@ -916,6 +946,7 @@ export declare const workerGitSecurityTestHarness: {
|
|
|
916
946
|
workspaceSyncFailureHydrationIsSafe: typeof workspaceSyncFailureHydrationIsSafe;
|
|
917
947
|
assertWorkerChildAdmission: typeof assertWorkerChildAdmission;
|
|
918
948
|
executeWriteFileOperation: typeof executeWriteFileOperation;
|
|
949
|
+
executeOperation: typeof executeOperation;
|
|
919
950
|
executeEditFileOperation: typeof executeEditFileOperation;
|
|
920
951
|
terminateCredentialBearingChildren: typeof terminateCredentialBearingChildren;
|
|
921
952
|
terminateCredentialBearingChildrenWithRetention: typeof terminateCredentialBearingChildrenWithRetention;
|
|
@@ -1010,6 +1041,14 @@ export declare function listWorkerDirectory(branchPath: string, inputPath?: stri
|
|
|
1010
1041
|
export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): WorkerCodeListResult;
|
|
1011
1042
|
export declare function readWorkerCodeFile(branchPath: string, inputPath: string): WorkerCodeReadResult;
|
|
1012
1043
|
export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerViewFileBytesResult;
|
|
1044
|
+
declare function executeOperation(input: {
|
|
1045
|
+
message: WorkerOperationServerMessage;
|
|
1046
|
+
resolvedTarget: ResolvedWorkerSessionTarget;
|
|
1047
|
+
baseUrl: string;
|
|
1048
|
+
token: string;
|
|
1049
|
+
rootDir: string;
|
|
1050
|
+
assertAdmission: () => void;
|
|
1051
|
+
}): Promise<WorkerOperationResult>;
|
|
1013
1052
|
declare function targetProcessEnv(rootDir: string, target: WorkerSessionTarget): Record<string, string>;
|
|
1014
1053
|
export declare function prepareShellEnvForTarget(input: {
|
|
1015
1054
|
target: WorkerSessionTarget;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PlanLintError {
|
|
2
|
+
planId: string;
|
|
3
|
+
message: string;
|
|
4
|
+
severity: "warning" | "error";
|
|
5
|
+
line?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PlanLintResult {
|
|
8
|
+
type: "plan_lint";
|
|
9
|
+
errors: PlanLintError[];
|
|
10
|
+
errorCount: number;
|
|
11
|
+
warningCount: number;
|
|
12
|
+
lintedPlanCount: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function lintPlanContent(planId: string, content: string): PlanLintError[];
|
|
15
|
+
export declare function lintPlans(plansDir: string, glob?: string): Promise<PlanLintResult>;
|
|
16
|
+
export declare function formatPlanLintErrors(errors: PlanLintError[]): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown plan parser.
|
|
3
|
+
*
|
|
4
|
+
* Plans are plain Markdown files. The only structured region is the first
|
|
5
|
+
* "## Tasks" section, which contains checkbox task lines.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PLAN_TASK_ID_REGEX: RegExp;
|
|
8
|
+
export declare const PLAN_TASK_LINE_REGEX: RegExp;
|
|
9
|
+
export declare const PLAN_TASKS_HEADING_REGEX: RegExp;
|
|
10
|
+
export interface PlanTask {
|
|
11
|
+
id: string;
|
|
12
|
+
description: string;
|
|
13
|
+
checked: boolean;
|
|
14
|
+
lineNumber: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ParsedPlan {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
content: string;
|
|
20
|
+
tasks: PlanTask[];
|
|
21
|
+
currentTask: PlanTask | null;
|
|
22
|
+
complete: boolean;
|
|
23
|
+
tasksStartLine: number | null;
|
|
24
|
+
tasksEndLine: number | null;
|
|
25
|
+
}
|
|
26
|
+
export type ParsedMarkdownPlan = ParsedPlan;
|
|
27
|
+
export declare function getPlanMetadata(content: string): {
|
|
28
|
+
title: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function getTasksSectionRange(content: string): {
|
|
31
|
+
startIndex: number;
|
|
32
|
+
endIndex: number;
|
|
33
|
+
} | null;
|
|
34
|
+
export declare function parseMarkdownPlan(content: string, planId: string): ParsedPlan;
|
|
35
|
+
export declare const parsePlan: typeof parseMarkdownPlan;
|
|
36
|
+
export declare function createPlanMarkdown(planId: string, title?: string): string;
|
|
37
|
+
export declare function updateTaskCheckbox(content: string, taskId: string, checked: boolean): string;
|
|
38
|
+
export declare function formatPlanTitle(planId: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type PlanTarget = {
|
|
2
|
+
type: "project";
|
|
3
|
+
} | {
|
|
4
|
+
type: "workspace";
|
|
5
|
+
rootProfile: "visible_projects" | "canonical_sync";
|
|
6
|
+
};
|
|
7
|
+
export declare const PLAN_EXTENSION = ".plan.md";
|
|
8
|
+
export declare function validatePlanId(planId: string): void;
|
|
9
|
+
export declare function assertPlanTarget(target: PlanTarget): void;
|
|
10
|
+
export declare function getWorkerPlansPath(target: PlanTarget): string;
|
|
11
|
+
/** The same worker path is used in prompts, injected reads, and file tools. */
|
|
12
|
+
export declare function getWorkerPlanPath(target: PlanTarget, planId: string): string;
|
|
13
|
+
export type WorkerReadPlanInput = {
|
|
14
|
+
planId: string;
|
|
15
|
+
createIfMissing: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type WorkerLintPlansInput = {
|
|
18
|
+
glob?: string;
|
|
19
|
+
};
|
|
20
|
+
export type WorkerUpdatePlanTaskInput = {
|
|
21
|
+
planId: string;
|
|
22
|
+
taskId: string;
|
|
23
|
+
checked: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type WorkerLintPlansResult = {
|
|
26
|
+
type: "lint_plans";
|
|
27
|
+
content: string;
|
|
28
|
+
};
|
|
29
|
+
export type WorkerUpdatePlanTaskResult = {
|
|
30
|
+
type: "update_task_status";
|
|
31
|
+
planId: string;
|
|
32
|
+
taskId: string;
|
|
33
|
+
checked: boolean;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.108",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/cjs/main.cjs",
|
|
6
6
|
"module": "./dist/mjs/main.mjs",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"r5d-worker": "dist/cjs/main.cjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"node-pty": "^1.1.0"
|
|
24
|
+
"node-pty": "^1.1.0",
|
|
25
|
+
"picomatch": "^4.0.3"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"dist",
|