@mestreyoda/fabrica 0.2.17 → 0.2.18
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 +2 -0
- package/defaults/fabrica/prompts/developer.md +16 -9
- package/dist/index.js +90 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,6 +120,8 @@ The plugin should load immediately after install, without manual remediation.
|
|
|
120
120
|
openclaw plugins inspect fabrica
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
If OpenClaw warns that `plugins.allow` is empty and non-bundled plugins may auto-load, that is a host trust-policy warning, not a Fabrica install failure. Fabrica can be installed and loadable while the OpenClaw operator still has to decide whether to keep open discovery or set an explicit trusted plugin list in `plugins.allow`.
|
|
124
|
+
|
|
123
125
|
**4. Configure Fabrica for a workspace**:
|
|
124
126
|
|
|
125
127
|
```bash
|
|
@@ -24,20 +24,25 @@ Read the comments carefully — they often contain clarifications, decisions, or
|
|
|
24
24
|
|
|
25
25
|
### 1. Use the assigned worktree
|
|
26
26
|
|
|
27
|
-
**NEVER work in the main checkout.**
|
|
27
|
+
**NEVER work in the main checkout.** The task message includes a `Repo:` / `Execution path:` line with the canonical repository path for this project. Start there first. If that path is missing, inaccessible, or points somewhere unexpected, stop and return `Work result: BLOCKED` instead of creating the project under another workspace.
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
# Example:
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# Example: task message says Repo: /home/ubuntu/git/acme/myproject
|
|
31
|
+
REPO_ROOT="/absolute/path/from-task-message"
|
|
32
|
+
cd "$REPO_ROOT"
|
|
33
33
|
BRANCH="feature/<issue-id>-<slug>"
|
|
34
34
|
WORKTREE="${REPO_ROOT}.worktrees/${BRANCH}"
|
|
35
|
-
git worktree
|
|
36
|
-
cd "$WORKTREE"
|
|
35
|
+
if git worktree list --porcelain | grep -Fq "worktree ${WORKTREE}"; then
|
|
36
|
+
cd "$WORKTREE"
|
|
37
|
+
else
|
|
38
|
+
git worktree add "$WORKTREE" -b "$BRANCH"
|
|
39
|
+
cd "$WORKTREE"
|
|
40
|
+
fi
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
The `.worktrees/` directory sits NEXT TO the repo folder (not inside it). This keeps the main checkout clean for the orchestrator and other workers. If the assigned worktree already exists from a previous task on the same branch, verify it's clean and reuse it.
|
|
40
|
-
|
|
44
|
+
|
|
45
|
+
Never create or implement the project under `~/.openclaw/workspace/<slug>` unless the task message explicitly says that directory is the canonical repo path. Once you are in the assigned worktree, stay there for the rest of the task and do not switch back to the main checkout.
|
|
41
46
|
|
|
42
47
|
### 2. Implement the changes
|
|
43
48
|
|
|
@@ -133,9 +138,11 @@ PR_NUM=$(gh pr list --head "$BRANCH" --json number -q '.[0].number')
|
|
|
133
138
|
QA_RAW=$(bash scripts/qa.sh 2>&1); QA_EXIT=$?
|
|
134
139
|
# MANDATORY: sanitize before embedding in PR — strip lines with tokens/keys/env vars/host paths
|
|
135
140
|
QA_OUTPUT=$(printf '%s' "$QA_RAW" | grep -v -iE '(TOKEN|SECRET|_KEY|PASSWORD|CREDENTIAL|PRIVATE|AUTH)=' | grep -v -E '(ghp_|gho_|github_pat_|sk-|xox[bprs]-|AIza|AKIA|glpat-)' | grep -v -E '^declare -x ' | grep -v -E '(/home/|~/.openclaw/)' | head -200)
|
|
136
|
-
|
|
141
|
+
REPO_SLUG=$(gh repo view --json nameWithOwner -q '.nameWithOwner')
|
|
142
|
+
CURRENT_BODY=$(gh api "repos/$REPO_SLUG/pulls/$PR_NUM" --jq '.body')
|
|
137
143
|
BODY_NO_QA=$(printf '%s' "$CURRENT_BODY" | perl -0pe 's/\n## QA Evidence\b[\s\S]*?(?=\n##\s|\z)//g')
|
|
138
|
-
|
|
144
|
+
NEW_BODY=$(printf '%s\n\n## QA Evidence\n\n```\n%s\n```\n\nExit code: %d\n' "$BODY_NO_QA" "$QA_OUTPUT" "$QA_EXIT")
|
|
145
|
+
gh api --method PATCH "repos/$REPO_SLUG/pulls/$PR_NUM" -f body="$NEW_BODY" >/dev/null
|
|
139
146
|
```
|
|
140
147
|
|
|
141
148
|
**NEVER bypass the sanitization step.** Never embed raw, unfiltered command output in PR descriptions.
|
package/dist/index.js
CHANGED
|
@@ -113905,8 +113905,8 @@ import fsSync from "node:fs";
|
|
|
113905
113905
|
import path5 from "node:path";
|
|
113906
113906
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
113907
113907
|
function getCurrentVersion() {
|
|
113908
|
-
if ("0.2.
|
|
113909
|
-
return "0.2.
|
|
113908
|
+
if ("0.2.18") {
|
|
113909
|
+
return "0.2.18";
|
|
113910
113910
|
}
|
|
113911
113911
|
try {
|
|
113912
113912
|
const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
|
|
@@ -126233,6 +126233,13 @@ async function getCurrentBranch(repoPath, runCommand) {
|
|
|
126233
126233
|
});
|
|
126234
126234
|
return result.stdout.trim();
|
|
126235
126235
|
}
|
|
126236
|
+
function isCurrentProjectBaseBranch(branchName, baseBranch) {
|
|
126237
|
+
const normalizedBranch = branchName.trim().toLowerCase();
|
|
126238
|
+
if (!normalizedBranch) return true;
|
|
126239
|
+
const normalizedBaseBranch = baseBranch?.trim().toLowerCase();
|
|
126240
|
+
if (normalizedBaseBranch) return normalizedBranch === normalizedBaseBranch;
|
|
126241
|
+
return ["main", "master", "develop", "development", "trunk"].includes(normalizedBranch);
|
|
126242
|
+
}
|
|
126236
126243
|
function throwInvalidQaEvidence(qaEvidence, actor) {
|
|
126237
126244
|
throw new Error(formatQaEvidenceValidationFailure(qaEvidence, actor));
|
|
126238
126245
|
}
|
|
@@ -126264,12 +126271,16 @@ async function isConflictResolutionCycle(workspaceDir, issueId, issueRuntime) {
|
|
|
126264
126271
|
}
|
|
126265
126272
|
return false;
|
|
126266
126273
|
}
|
|
126267
|
-
async function validatePrExistsForDeveloper(issueId, repoPath, provider, runCommand, workspaceDir, projectSlug, issueRuntime) {
|
|
126274
|
+
async function validatePrExistsForDeveloper(issueId, repoPath, provider, runCommand, workspaceDir, projectSlug, issueRuntime, baseBranch) {
|
|
126268
126275
|
const logger6 = getRootLogger().child({ issueId, phase: "work-finish" });
|
|
126269
126276
|
const branchName = await getCurrentBranch(repoPath, runCommand).catch(() => "");
|
|
126270
126277
|
try {
|
|
126271
|
-
const
|
|
126272
|
-
const
|
|
126278
|
+
const preferIssuePr = isCurrentProjectBaseBranch(branchName, baseBranch);
|
|
126279
|
+
const branchPr = !preferIssuePr ? await provider.findOpenPrForBranch(branchName) : null;
|
|
126280
|
+
const issuePr = await provider.getPrStatus(issueId);
|
|
126281
|
+
const issuePrIsReviewable = !!issuePr.url && issuePr.state !== PrState.MERGED && issuePr.state !== PrState.CLOSED;
|
|
126282
|
+
const branchPrIsReviewable = !!branchPr?.url && branchPr.state !== PrState.MERGED && branchPr.state !== PrState.CLOSED;
|
|
126283
|
+
const prStatus = preferIssuePr ? issuePrIsReviewable ? issuePr : branchPr ?? issuePr : branchPrIsReviewable ? branchPr : issuePr;
|
|
126273
126284
|
if (!prStatus.url || prStatus.state === PrState.MERGED || prStatus.state === PrState.CLOSED) {
|
|
126274
126285
|
const currentBranch = branchName || "current-branch";
|
|
126275
126286
|
const reason = !prStatus.url ? `\u2717 No PR found for branch: ${currentBranch}` : prStatus.state === PrState.MERGED ? `\u2717 Last linked PR is already merged: ${prStatus.url}` : `\u2717 Last linked PR is closed and not reviewable: ${prStatus.url}`;
|
|
@@ -126553,7 +126564,16 @@ function createWorkFinishTool(ctx) {
|
|
|
126553
126564
|
const pluginConfig = ctx.pluginConfig;
|
|
126554
126565
|
let developerPrStatus;
|
|
126555
126566
|
if (role === "developer" && result === "done") {
|
|
126556
|
-
developerPrStatus = await validatePrExistsForDeveloper(
|
|
126567
|
+
developerPrStatus = await validatePrExistsForDeveloper(
|
|
126568
|
+
issueId,
|
|
126569
|
+
repoPath,
|
|
126570
|
+
provider,
|
|
126571
|
+
ctx.runCommand,
|
|
126572
|
+
workspaceDir,
|
|
126573
|
+
project.slug,
|
|
126574
|
+
project.issueRuntime?.[String(issueId)],
|
|
126575
|
+
project.baseBranch
|
|
126576
|
+
);
|
|
126557
126577
|
await updateIssueRuntime(workspaceDir, project.slug, issueId, {
|
|
126558
126578
|
currentPrNodeId: developerPrStatus.nodeId ?? null,
|
|
126559
126579
|
currentPrNumber: developerPrStatus.number ?? null,
|
|
@@ -128021,7 +128041,6 @@ async function resolveEffectiveModelForGateway(requested, runCommand) {
|
|
|
128021
128041
|
// lib/dispatch/message-builder.ts
|
|
128022
128042
|
init_roles();
|
|
128023
128043
|
function buildTaskMessage(opts) {
|
|
128024
|
-
const sanitizeRepoContext = (value) => value.startsWith("/") || value.startsWith("~/") ? "[repository workspace hidden]" : value;
|
|
128025
128044
|
const {
|
|
128026
128045
|
projectName,
|
|
128027
128046
|
channelId,
|
|
@@ -128033,7 +128052,7 @@ function buildTaskMessage(opts) {
|
|
|
128033
128052
|
repo,
|
|
128034
128053
|
baseBranch
|
|
128035
128054
|
} = opts;
|
|
128036
|
-
const repoDisplay =
|
|
128055
|
+
const repoDisplay = repo;
|
|
128037
128056
|
const isFeedbackCycle = !!opts.prFeedback;
|
|
128038
128057
|
const parts = [
|
|
128039
128058
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
@@ -128097,13 +128116,14 @@ ${issueDescription}` : ""
|
|
|
128097
128116
|
parts.push(
|
|
128098
128117
|
``,
|
|
128099
128118
|
`Repo: ${repoDisplay} | Branch: ${baseBranch} | ${issueUrl}`,
|
|
128100
|
-
`Project: ${projectName} | Channel: ${channelId}
|
|
128119
|
+
`Project: ${projectName} | Channel: ${channelId}`,
|
|
128120
|
+
`Execution path: ${repoDisplay}`,
|
|
128121
|
+
`Start by changing into the canonical repo path above before creating or reusing a worktree. Do not create or implement the project under ~/.openclaw/workspace unless the repo path itself points there.`
|
|
128101
128122
|
);
|
|
128102
128123
|
parts.push(...buildCompletionContract(role));
|
|
128103
128124
|
return parts.join("\n");
|
|
128104
128125
|
}
|
|
128105
128126
|
function buildConflictFixMessage(opts) {
|
|
128106
|
-
const sanitizeRepoContext = (value) => value.startsWith("/") || value.startsWith("~/") ? "[repository workspace hidden]" : value;
|
|
128107
128127
|
const {
|
|
128108
128128
|
projectName,
|
|
128109
128129
|
channelId,
|
|
@@ -128115,7 +128135,7 @@ function buildConflictFixMessage(opts) {
|
|
|
128115
128135
|
baseBranch,
|
|
128116
128136
|
prFeedback
|
|
128117
128137
|
} = opts;
|
|
128118
|
-
const repoDisplay =
|
|
128138
|
+
const repoDisplay = repo;
|
|
128119
128139
|
const parts = [
|
|
128120
128140
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
128121
128141
|
``,
|
|
@@ -128127,7 +128147,9 @@ function buildConflictFixMessage(opts) {
|
|
|
128127
128147
|
parts.push(
|
|
128128
128148
|
``,
|
|
128129
128149
|
`Repo: ${repoDisplay} | Branch: ${baseBranch} | ${issueUrl}`,
|
|
128130
|
-
`Project: ${projectName} | Channel: ${channelId}
|
|
128150
|
+
`Project: ${projectName} | Channel: ${channelId}`,
|
|
128151
|
+
`Execution path: ${repoDisplay}`,
|
|
128152
|
+
`Start by changing into the canonical repo path above before reusing the PR branch or creating its worktree. Do not resolve the issue inside ~/.openclaw/workspace unless the repo path itself points there.`
|
|
128131
128153
|
);
|
|
128132
128154
|
parts.push(...buildCompletionContract(role));
|
|
128133
128155
|
return parts.join("\n");
|
|
@@ -128717,7 +128739,7 @@ async function dispatchTask(opts) {
|
|
|
128717
128739
|
const securityChecklist = await loadSecurityChecklist(workspaceDir, project.name).catch(() => "");
|
|
128718
128740
|
const primaryChannelId = project.slug;
|
|
128719
128741
|
const isConflictFix = prFeedback?.reason === "merge_conflict";
|
|
128720
|
-
const repoContext = project.repoRemote?.replace(/\.git$/, "")
|
|
128742
|
+
const repoContext = project.repo ? resolveRepoPath(project.repo) : project.repoRemote?.replace(/\.git$/, "") || project.slug;
|
|
128721
128743
|
const taskMessage = isConflictFix && prFeedback ? buildConflictFixMessage({
|
|
128722
128744
|
projectName: project.name,
|
|
128723
128745
|
channelId: primaryChannelId,
|
|
@@ -130417,6 +130439,7 @@ init_labels();
|
|
|
130417
130439
|
// lib/services/worker-completion.ts
|
|
130418
130440
|
init_audit();
|
|
130419
130441
|
import fs24 from "node:fs/promises";
|
|
130442
|
+
init_workflow();
|
|
130420
130443
|
|
|
130421
130444
|
// lib/services/worker-result.ts
|
|
130422
130445
|
var ROLE_PREFIX = {
|
|
@@ -130823,7 +130846,8 @@ async function defaultValidateDeveloperDone(opts) {
|
|
|
130823
130846
|
opts.runCommand,
|
|
130824
130847
|
opts.workspaceDir,
|
|
130825
130848
|
opts.projectSlug,
|
|
130826
|
-
opts.issueRuntime
|
|
130849
|
+
opts.issueRuntime,
|
|
130850
|
+
opts.baseBranch
|
|
130827
130851
|
);
|
|
130828
130852
|
return { ok: true, prStatus };
|
|
130829
130853
|
} catch (error48) {
|
|
@@ -130915,19 +130939,68 @@ async function applyWorkerResult(opts) {
|
|
|
130915
130939
|
runCommand: opts.runCommand,
|
|
130916
130940
|
workspaceDir: opts.workspaceDir,
|
|
130917
130941
|
projectSlug: context2.projectSlug,
|
|
130918
|
-
issueRuntime: context2.issueRuntime
|
|
130942
|
+
issueRuntime: context2.issueRuntime,
|
|
130943
|
+
baseBranch: context2.project.baseBranch
|
|
130919
130944
|
});
|
|
130920
130945
|
if (!validation.ok) {
|
|
130946
|
+
const validationReason = validation.reason ?? "developer_validation_failed";
|
|
130947
|
+
const feedbackQueueLabel = getQueueLabels(workflow, "developer").find((label) => isFeedbackState(workflow, label)) ?? "To Improve";
|
|
130921
130948
|
await log(opts.workspaceDir, "worker_completion_skipped", {
|
|
130922
130949
|
sessionKey: context2.project.workers[context2.parsed.role]?.levels?.[context2.slotLevel]?.[context2.slotIndex]?.sessionKey ?? null,
|
|
130923
130950
|
projectSlug: context2.projectSlug,
|
|
130924
130951
|
issueId: context2.issueId,
|
|
130925
130952
|
role: context2.parsed.role,
|
|
130926
130953
|
result: opts.result.value,
|
|
130927
|
-
reason:
|
|
130954
|
+
reason: validationReason
|
|
130955
|
+
}).catch(() => {
|
|
130956
|
+
});
|
|
130957
|
+
await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
|
|
130958
|
+
inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
130959
|
+
inconclusiveCompletionReason: validationReason
|
|
130960
|
+
}).catch(() => {
|
|
130961
|
+
});
|
|
130962
|
+
await executeCompletion({
|
|
130963
|
+
workspaceDir: opts.workspaceDir,
|
|
130964
|
+
projectSlug: context2.projectSlug,
|
|
130965
|
+
role: context2.parsed.role,
|
|
130966
|
+
result: "blocked",
|
|
130967
|
+
issueId: context2.issueId,
|
|
130968
|
+
summary: `Automatic recovery: developer reported DONE but completion validation failed.
|
|
130969
|
+
|
|
130970
|
+
${validationReason}`,
|
|
130971
|
+
provider,
|
|
130972
|
+
repoPath,
|
|
130973
|
+
projectName: context2.project.name,
|
|
130974
|
+
channels: context2.project.channels,
|
|
130975
|
+
runtime: opts.runtime,
|
|
130976
|
+
workflow,
|
|
130977
|
+
level: context2.slotLevel,
|
|
130978
|
+
slotIndex: context2.slotIndex,
|
|
130979
|
+
overrideToLabel: feedbackQueueLabel,
|
|
130980
|
+
overrideReason: validationReason,
|
|
130981
|
+
runCommand: opts.runCommand
|
|
130982
|
+
});
|
|
130983
|
+
await recordIssueLifecycle({
|
|
130984
|
+
workspaceDir: opts.workspaceDir,
|
|
130985
|
+
slug: context2.projectSlug,
|
|
130986
|
+
issueId: context2.issueId,
|
|
130987
|
+
stage: "session_completed",
|
|
130988
|
+
sessionKey: context2.sessionKey,
|
|
130989
|
+
details: { role: context2.parsed.role, result: "blocked", source: "agent_end_recovery", reason: validationReason }
|
|
130990
|
+
}).catch(() => {
|
|
130991
|
+
});
|
|
130992
|
+
await log(opts.workspaceDir, "worker_completion_applied", {
|
|
130993
|
+
sessionKey: context2.sessionKey,
|
|
130994
|
+
projectSlug: context2.projectSlug,
|
|
130995
|
+
issueId: context2.issueId,
|
|
130996
|
+
role: context2.parsed.role,
|
|
130997
|
+
result: "BLOCKED",
|
|
130998
|
+
source: "agent_end_recovery",
|
|
130999
|
+
recoveredTo: "To Improve",
|
|
131000
|
+
reason: validationReason
|
|
130928
131001
|
}).catch(() => {
|
|
130929
131002
|
});
|
|
130930
|
-
return { applied:
|
|
131003
|
+
return { applied: true };
|
|
130931
131004
|
}
|
|
130932
131005
|
validatedPrStatus = validation.prStatus;
|
|
130933
131006
|
await persistDeveloperPrBinding({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mestreyoda/fabrica",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "Autonomous software engineering pipeline for OpenClaw. Turns ideas into deployed code via intake, dispatch, review, test, and merge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|