@papi-ai/server 0.7.80 → 0.7.81
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/backfill-cycle-metrics.js +107 -1
- package/dist/index.js +875 -57
- package/dist/prompts.js +36 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ __export(git_exports, {
|
|
|
29
29
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
30
30
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
31
31
|
getBranchDiff: () => getBranchDiff,
|
|
32
|
+
getCommitFiles: () => getCommitFiles,
|
|
32
33
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
33
34
|
getCurrentBranch: () => getCurrentBranch,
|
|
34
35
|
getDocPathsTouchedOnBranch: () => getDocPathsTouchedOnBranch,
|
|
@@ -42,11 +43,13 @@ __export(git_exports, {
|
|
|
42
43
|
getModifiedFiles: () => getModifiedFiles,
|
|
43
44
|
getOriginRepoSlug: () => getOriginRepoSlug,
|
|
44
45
|
getOriginUrl: () => getOriginUrl,
|
|
46
|
+
getPathsDifferingFrom: () => getPathsDifferingFrom,
|
|
45
47
|
getPullRequestUrl: () => getPullRequestUrl,
|
|
46
48
|
getRemoteBranchFiles: () => getRemoteBranchFiles,
|
|
47
49
|
getRootCommitHash: () => getRootCommitHash,
|
|
48
50
|
getStagedFiles: () => getStagedFiles,
|
|
49
51
|
getTagTarget: () => getTagTarget,
|
|
52
|
+
getTaskDiff: () => getTaskDiff,
|
|
50
53
|
getTaskIdsOnBranch: () => getTaskIdsOnBranch,
|
|
51
54
|
getTrackedModifiedFiles: () => getTrackedModifiedFiles,
|
|
52
55
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
@@ -272,6 +275,50 @@ function getBranchDiff(cwd, base = "origin/main", maxBytes = 2e5) {
|
|
|
272
275
|
}
|
|
273
276
|
return "";
|
|
274
277
|
}
|
|
278
|
+
function getTaskDiff(cwd, taskId, base = "origin/main", maxBytes = 2e5) {
|
|
279
|
+
const truncate2 = (out) => out.length > maxBytes ? `${out.slice(0, maxBytes)}
|
|
280
|
+
|
|
281
|
+
... [diff truncated at ${Math.round(maxBytes / 1024)} KB]` : out;
|
|
282
|
+
try {
|
|
283
|
+
const shas = execFileSync(
|
|
284
|
+
"git",
|
|
285
|
+
["log", "--all", "--format=%H", `--grep=${taskId})`, "--fixed-strings"],
|
|
286
|
+
{ cwd, encoding: "utf-8", maxBuffer: 8 * 1024 * 1024 }
|
|
287
|
+
).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
288
|
+
if (shas.length > 0) {
|
|
289
|
+
const parts = [];
|
|
290
|
+
for (const sha of [...shas].reverse()) {
|
|
291
|
+
try {
|
|
292
|
+
const one = execFileSync("git", ["diff", `${sha}^..${sha}`], {
|
|
293
|
+
cwd,
|
|
294
|
+
encoding: "utf-8",
|
|
295
|
+
maxBuffer: 32 * 1024 * 1024
|
|
296
|
+
});
|
|
297
|
+
if (one) parts.push(one);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const out = parts.join("\n");
|
|
302
|
+
if (out) {
|
|
303
|
+
const range = shas.length === 1 ? shas[0].slice(0, 8) : `${shas[shas.length - 1].slice(0, 8)}\u2026${shas[0].slice(0, 8)}`;
|
|
304
|
+
return {
|
|
305
|
+
diff: truncate2(out),
|
|
306
|
+
scope: "task-commits",
|
|
307
|
+
detail: `${shas.length} commit(s) for ${taskId} (${range}), each against its own parent`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
const branch = getCurrentBranch(cwd);
|
|
314
|
+
const diff = getBranchDiff(cwd, base, maxBytes);
|
|
315
|
+
if (!diff) return { diff: "", scope: "none", detail: "no diff resolved" };
|
|
316
|
+
return {
|
|
317
|
+
diff,
|
|
318
|
+
scope: "whole-branch",
|
|
319
|
+
detail: `no commit naming ${taskId} was found, so this is the ENTIRE diff of ${branch ?? "the current branch"} vs ${base} \u2014 it may include other tasks' work, and may not include this task's`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
275
322
|
function getHeadCommitSubject(cwd) {
|
|
276
323
|
try {
|
|
277
324
|
const out = execFileSync("git", ["log", "-1", "--format=%s"], {
|
|
@@ -319,6 +366,28 @@ function branchExists(cwd, branch) {
|
|
|
319
366
|
return false;
|
|
320
367
|
}
|
|
321
368
|
}
|
|
369
|
+
function getPathsDifferingFrom(cwd, target) {
|
|
370
|
+
try {
|
|
371
|
+
const out = execFileSync("git", ["diff", "--name-only", "HEAD", target], {
|
|
372
|
+
cwd,
|
|
373
|
+
encoding: "utf-8"
|
|
374
|
+
});
|
|
375
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function getCommitFiles(cwd, ref = "HEAD") {
|
|
381
|
+
try {
|
|
382
|
+
const out = execFileSync("git", ["show", "--name-only", "--pretty=format:", ref], {
|
|
383
|
+
cwd,
|
|
384
|
+
encoding: "utf-8"
|
|
385
|
+
});
|
|
386
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
322
391
|
function checkoutBranch(cwd, branch) {
|
|
323
392
|
try {
|
|
324
393
|
execFileSync("git", ["checkout", branch], { cwd, encoding: "utf-8" });
|
|
@@ -1329,7 +1398,7 @@ var init_proxy_adapter = __esm({
|
|
|
1329
1398
|
"listContributorReleasePrs",
|
|
1330
1399
|
"claimReview",
|
|
1331
1400
|
"getSiblingAds",
|
|
1332
|
-
"getSiblingRepoTasks"
|
|
1401
|
+
"getSiblingRepoTasks",
|
|
1333
1402
|
// task-2828 (C339): attributed-intelligence analytics reader — pg-only that cycle.
|
|
1334
1403
|
// task-2864 (C343): WIRED. getModelOutcomeStats now has an edge case handler (raw
|
|
1335
1404
|
// SQL via postgres.js mirroring the pg query + inlined computeModelOutcomes bucketing)
|
|
@@ -1353,6 +1422,28 @@ var init_proxy_adapter = __esm({
|
|
|
1353
1422
|
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
1354
1423
|
// hosted callers and persists a project-scoped cycle_progress_steps row. Removed
|
|
1355
1424
|
// from NO_FORWARD (was the task-2484 pg-only gap) — hosted parity restored.
|
|
1425
|
+
//
|
|
1426
|
+
// (3) pg-only optimisations probed STRUCTURALLY with a local fallback beside them.
|
|
1427
|
+
// These are the dangerous class: the caller asks `typeof adapter.X === 'function'`
|
|
1428
|
+
// to decide whether the fast path exists, and the get-trap answers "yes" for any
|
|
1429
|
+
// name absent from this set. The probe then passes, the call forwards, and the
|
|
1430
|
+
// edge 403s — while a working fallback sits a few lines below, unreachable.
|
|
1431
|
+
// A method belongs here when BOTH are true: it is probed by `typeof` rather than
|
|
1432
|
+
// called unconditionally, and the probe's else-branch is a real fallback.
|
|
1433
|
+
//
|
|
1434
|
+
// task-3290 (C361): allocateActiveDecision + applyActiveDecisionUpdates are pg-only
|
|
1435
|
+
// (atomic id allocation; transactional batch apply). Both are probed in
|
|
1436
|
+
// services/strategy.ts — asDecisionIdAllocator and asDecisionBatchApplier — and both
|
|
1437
|
+
// have read-then-write / sequential fallbacks. Hosted users hit a 403 minting ANY
|
|
1438
|
+
// Active Decision via setup's AD seed or strategy_change until these were listed.
|
|
1439
|
+
"allocateActiveDecision",
|
|
1440
|
+
"applyActiveDecisionUpdates",
|
|
1441
|
+
// task-3290 (C361), same sweep: getLastZoomOutCycle was documented in the parity
|
|
1442
|
+
// ledger as "proxy NO_FORWARD → safe no-op" while NOT being in this set. It is
|
|
1443
|
+
// optional-chain probed in services/health.ts and wrapped in try/catch, so it
|
|
1444
|
+
// degraded rather than crashed — but every hosted `orient` paid a round-trip to
|
|
1445
|
+
// earn a silent 403. Listing it makes the ledger's claim true and skips the trip.
|
|
1446
|
+
"getLastZoomOutCycle"
|
|
1356
1447
|
]);
|
|
1357
1448
|
ProxyPapiAdapter = class _ProxyPapiAdapter {
|
|
1358
1449
|
endpoint;
|
|
@@ -1884,6 +1975,21 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1884
1975
|
updateDogfoodEntryStatus(id, status, linkedTaskId) {
|
|
1885
1976
|
return this.invoke("updateDogfoodEntryStatus", [id, status, linkedTaskId]);
|
|
1886
1977
|
}
|
|
1978
|
+
// --- Project conventions (task-3271) ---
|
|
1979
|
+
//
|
|
1980
|
+
// Wired to the edge handler from the start rather than parked in NO_FORWARD.
|
|
1981
|
+
// The hosted remote connector is the only install path an external user has,
|
|
1982
|
+
// so a local-only conventions store would be a feature nobody in the actual
|
|
1983
|
+
// user base can reach.
|
|
1984
|
+
listConventions() {
|
|
1985
|
+
return this.invoke("listConventions");
|
|
1986
|
+
}
|
|
1987
|
+
createConvention(convention) {
|
|
1988
|
+
return this.invoke("createConvention", [convention]);
|
|
1989
|
+
}
|
|
1990
|
+
deleteConvention(id) {
|
|
1991
|
+
return this.invoke("deleteConvention", [id]);
|
|
1992
|
+
}
|
|
1887
1993
|
// --- Harness inventory (task-1896) ---
|
|
1888
1994
|
getHarnessInventory() {
|
|
1889
1995
|
return this.invoke("getHarnessInventory");
|
|
@@ -7407,6 +7513,24 @@ If a candidate AD body could be invalidated by running a SQL query, refreshing a
|
|
|
7407
7513
|
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
7408
7514
|
|
|
7409
7515
|
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
7516
|
+
var AD_ADMISSION_RULES = `**AD Admission Rule \u2014 PROPOSE a decision when the project takes a real stance.**
|
|
7517
|
+
|
|
7518
|
+
The guard above says what to reject. This says what to propose. Rejecting is not the safe default: a stance that never gets minted is a stance the next cycle cannot see, and re-deciding it every session is the exact cost this project is paying you to remove. When a candidate passes all four tests below, propose it \u2014 do not wait for the next strategy review.
|
|
7519
|
+
|
|
7520
|
+
**The four tests. All four must pass.**
|
|
7521
|
+
(a) **Alternatives were real.** Something else could genuinely have been chosen. If there was only ever one way to do it, it is not a decision.
|
|
7522
|
+
(b) **It constrains future work not yet scoped.** It changes what a task nobody has written yet will do. Not "it describes work we did" \u2014 "it binds work we have not planned".
|
|
7523
|
+
(c) **It is arguable today.** A competent person could argue the other side right now, with the evidence currently available. Not "was once debated" \u2014 live.
|
|
7524
|
+
(d) **Reversing it costs more than making it did.** If undoing it is as cheap as doing it, nothing is being constrained.
|
|
7525
|
+
|
|
7526
|
+
**Routing for a near-miss \u2014 a candidate that fails one test still goes somewhere.**
|
|
7527
|
+
- **Fails (c) only** \u2014 real alternatives, binds future work, expensive to reverse, but nobody is arguing the other side any more: it is a **Convention**, not a Decision. A Convention is a settled answer to a recurring question; it does not need adjudicating, it needs to be *known* by whoever builds next. Record it as a project convention so it rides future build handoffs. Do not mint it as an AD, and do not drop it.
|
|
7528
|
+
- **Fails (b)** \u2014 it does not constrain any future work: it is not durable at all, it is just work. Capture it as a task, a build report note, or a doc. Do not mint it.
|
|
7529
|
+
- **Fails (a) or (d)** \u2014 not a stance. Same routing as (b): capture it, do not mint it.
|
|
7530
|
+
|
|
7531
|
+
**Copy variants are NEVER decisions.** A tagline, a headline, a value proposition, a piece of marketing or product wording: swapping one for another constrains no future work, so it fails test (b) outright. Wording changes as often as the market teaches you something, and routing every edit through decision ceremony is what buries the handful of decisions that genuinely constrain the project. Edit the wording where the wording lives. If two variants are being compared against a measurement, that is an experiment, not a decision.
|
|
7532
|
+
|
|
7533
|
+
**A decision decaying into a convention is the healthy path, not a failure.** When a live AD stops being arguable \u2014 the alternatives are no longer on the table and nobody would re-litigate it \u2014 say so during housekeeping and propose retiring it into a convention. An AD registry where most entries are settled is one nobody reads.`;
|
|
7410
7534
|
var AD_CONFLICT_SURFACING_RULES = `**A contradiction is NOT a veto \u2014 surface it, never silently shelve it.**
|
|
7411
7535
|
|
|
7412
7536
|
Active Decisions are *active*: they can be superseded, modified, or abandoned. You do NOT have authority to kill a piece of work simply because it cuts against one. That is the user's call, and they can only make it if you show it to them.
|
|
@@ -7653,7 +7777,7 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
7653
7777
|
var PLAN_FRAGMENT_DESIGN_BRIEF = `
|
|
7654
7778
|
**Design brief task detection:** When a task's task type is "design-brief", generate a DESIGN BRIEF handoff. Inside SCOPE (DO THIS), use these type-specific subsections. KEEP the "SCOPE (DO THIS)" header and put these subsections INSIDE it \u2014 the handoff parser only recognises the standard headers (packages/adapter-md build-handoff.ts SECTION_HEADERS) and a handoff with no SCOPE section is rejected outright (task-3050, C357):
|
|
7655
7779
|
- AUDIENCE: Who this design is for \u2014 persona and context of use (e.g. "non-technical Owner, first dashboard visit")
|
|
7656
|
-
- BRAND CONSTRAINTS: Palette, typography, tone \u2014 pull from
|
|
7780
|
+
- BRAND CONSTRAINTS: Palette, typography, tone \u2014 pull from \`PRODUCT.md\` (strategic: brand, users, product purpose, design principles) AND \`DESIGN.md\` (visual tokens: palette, typography, elevation, components) if present. If neither exists, state "No brand doc \u2014 Owner should define constraints before starting."
|
|
7657
7781
|
- DELIVERABLE FORMAT: What the output looks like \u2014 design handoff package / annotated mockup / style spec. Be specific so the person doing the work knows what "done" means.
|
|
7658
7782
|
- REVIEW POINTS: What the Owner must approve before the design is considered done (e.g. layout, copy, colour, imagery).
|
|
7659
7783
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION sections as normal.
|
|
@@ -7685,11 +7809,11 @@ var PLAN_FRAGMENT_OPS_BRIEF = `
|
|
|
7685
7809
|
var PLAN_FRAGMENT_UI = `
|
|
7686
7810
|
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
7687
7811
|
When a task IS a UI task (primary scope is visual/frontend):
|
|
7688
|
-
- Add to SCOPE: "Read
|
|
7812
|
+
- Add to SCOPE: "Read \`PRODUCT.md\` for product purpose, users and design principles, and \`DESIGN.md\` for the visual tokens (palette, typography, elevation, components) \u2014 these are the two files the \`impeccable\` skill reads, and every visual decision must align with them. Run \`impeccable init\` to create them if they do not exist. Use the \`frontend-design\` skill for implementation."
|
|
7689
7813
|
- For M/L UI tasks, add to SCOPE: "Use the full impeccable workflow: shape (direction approval) \u2192 craft (design+build via \`impeccable craft\` / frontend-design) \u2192 live (in-browser HMR iteration via \`impeccable live\`) \u2192 detect (slop check). The approved direction is the quality bar; expect 2-3 iterations. Playground is for pre-build direction approval of shareable/static artifacts or non-dashboard explorers; Playwright is for post-build verification, not mid-design iteration."
|
|
7690
7814
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
7691
|
-
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection \u2014 pull from \`
|
|
7692
|
-
The planner's job is scoping, not design direction. Design decisions happen at build time via
|
|
7815
|
+
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection \u2014 pull from \`PRODUCT.md\` and \`DESIGN.md\` for canonical brand identity."
|
|
7816
|
+
The planner's job is scoping, not design direction. Design decisions happen at build time via \`PRODUCT.md\` (product purpose, users, design principles) + \`DESIGN.md\` (visual tokens) and the frontend-design skill \u2014 don't try to write design specs in the handoff.`;
|
|
7693
7817
|
var PLAN_FRAGMENT_PRODUCT_BRIEF = `
|
|
7694
7818
|
12. **Product Brief** \u2014 Check whether the product brief still reflects reality. Update the brief when ANY of these apply:
|
|
7695
7819
|
- A new AD was created or an existing AD was superseded that changes product scope, target user, or positioning
|
|
@@ -7783,6 +7907,8 @@ ${AD_CONFLICT_SURFACING_RULES}
|
|
|
7783
7907
|
|
|
7784
7908
|
${AD_REJECTION_RULES}
|
|
7785
7909
|
|
|
7910
|
+
${AD_ADMISSION_RULES}
|
|
7911
|
+
|
|
7786
7912
|
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
7787
7913
|
|
|
7788
7914
|
### Operational Quality Rules
|
|
@@ -8325,6 +8451,8 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
8325
8451
|
|
|
8326
8452
|
${AD_REJECTION_RULES}
|
|
8327
8453
|
|
|
8454
|
+
${AD_ADMISSION_RULES}
|
|
8455
|
+
|
|
8328
8456
|
**Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
|
|
8329
8457
|
|
|
8330
8458
|
**Doc Action Staleness:** If a "### Doc Action Staleness" section is present, treat it as a research-to-action audit. For each entry:
|
|
@@ -8673,6 +8801,8 @@ The body field must be the COMPLETE replacement text for the AD block (including
|
|
|
8673
8801
|
|
|
8674
8802
|
${AD_REJECTION_RULES}
|
|
8675
8803
|
|
|
8804
|
+
${AD_ADMISSION_RULES}
|
|
8805
|
+
|
|
8676
8806
|
## PHASE UPDATES
|
|
8677
8807
|
|
|
8678
8808
|
If the strategic change affects the project's phase structure, include a phaseUpdates array.
|
|
@@ -8845,10 +8975,15 @@ function buildPreScanInstruction(opts) {
|
|
|
8845
8975
|
const docRouting = opts.hosted ? "paste the relevant content (decision records, roadmap, spec excerpts) directly into `description` \u2014 over a remote connector PAPI cannot read local file paths." : "pass local doc file paths as `sources` (comma-separated), or paste content into `description`.";
|
|
8846
8976
|
return [
|
|
8847
8977
|
`**\u{1F50E} Before you generate anything \u2014 scan the project and gather real context.**`,
|
|
8848
|
-
|
|
8978
|
+
// task-3293: state the division of labour ONCE, up front. A user watching this
|
|
8979
|
+
// session sees PAPI say it cannot read their files and then sees their agent
|
|
8980
|
+
// read their files. That reads as a contradiction, or worse, unless it is named
|
|
8981
|
+
// first: PAPI never touches the filesystem, YOU do, and you send back a summary.
|
|
8982
|
+
`**How this works:** PAPI has no access to your machine and never reads your files. You (the agent running this) do the reading locally and send back a short summary. Nothing is uploaded except what you pass in that summary.`,
|
|
8983
|
+
`A brief and Active Decisions built from the real code and docs are far sharper than ones guessed from a project name.`,
|
|
8849
8984
|
``,
|
|
8850
8985
|
`1. **Confirm the environment.** Check that you are running from the root of this project's dev environment \u2014 the folder that holds its source, git history, and docs. If you are not there, or you are connected over a remote connector with no access to the user's files, say so and ask the user where the project lives before continuing.`,
|
|
8851
|
-
`2. **Scan the code.** List the top-level directories and files, read the package/build manifest and the README (first ~3000 chars), and collect recent commit subjects. Pass this back as \`codebase_scan\` on the next \`setup\` call.`,
|
|
8986
|
+
`2. **Scan the code.** List the top-level directories and files, read the package/build manifest and the README (first ~3000 chars), and collect recent commit subjects. Pass this back as \`codebase_scan\` on the next \`setup\` call. **Never include secrets** \u2014 skip \`.env\` and any credential, key or certificate file, and do not paste tokens, passwords or connection strings into the summary. PAPI also strips secret-shaped values server-side, but do not rely on that: leave them out.`,
|
|
8852
8987
|
`3. **Scan the docs.** Look through \`docs/\`, design notes, ADRs/decision records, and any roadmap or planning files \u2014 not just code.`,
|
|
8853
8988
|
`4. **Check for sibling / separate repos.** Many projects span more than one repo (a separate frontend, backend, infra, or mobile repo, or other packages in a monorepo). Ask the user whether any related repos or directories exist, and scan those too.`,
|
|
8854
8989
|
`5. **Ask the user to point you at extra context.** Prompt them: "Is there anything else I should read before setting this up \u2014 a PRD or spec, decision records, a roadmap, or links to related repos or docs?" Fold whatever they share into the scan.`,
|
|
@@ -12345,7 +12480,9 @@ async function buildSessionGuidance(callerKey) {
|
|
|
12345
12480
|
|
|
12346
12481
|
// src/services/onboarding-coaching.ts
|
|
12347
12482
|
var ONBOARDING_EARLY_CYCLE_MAX = 2;
|
|
12483
|
+
var ONBOARDING_FIRST_CYCLE_MAX = 1;
|
|
12348
12484
|
var MAX_COACHING_LINES = 4;
|
|
12485
|
+
var COACH_EXPLAIN_VALUE = "First cycle: before you run each step, tell the user in one line what it produces and why it is worth the wait, then run it. Say what it means for their project, not what the tool does.";
|
|
12349
12486
|
var COACH_CONNECT_REPO = "No repository is linked to this project yet. Link your repo from the dashboard Settings (or capture it during `setup`) so builds, reviews, and releases attach to the right codebase.";
|
|
12350
12487
|
var COACH_ROOT_DIR = "Before building, confirm this session is running from your project root directory, so commits and builds land against the right files.";
|
|
12351
12488
|
var COACH_CLICKABLE_TASKS = "Task cards on your dashboard expand on click. Open one to read its full build handoff, comments, and history.";
|
|
@@ -12357,10 +12494,14 @@ var ONBOARDING_COACHING_HEADING = "## Getting Started";
|
|
|
12357
12494
|
function buildOnboardingCoaching(state) {
|
|
12358
12495
|
const lines = [];
|
|
12359
12496
|
const isEarly = state.cycleNumber <= ONBOARDING_EARLY_CYCLE_MAX;
|
|
12497
|
+
const isFirstRun = state.cycleNumber <= ONBOARDING_FIRST_CYCLE_MAX;
|
|
12360
12498
|
const { surface } = state;
|
|
12361
12499
|
if (state.repoConnected === false && (surface === "orient" || surface === "plan")) {
|
|
12362
12500
|
lines.push(COACH_CONNECT_REPO);
|
|
12363
12501
|
}
|
|
12502
|
+
if (isFirstRun) {
|
|
12503
|
+
lines.push(COACH_EXPLAIN_VALUE);
|
|
12504
|
+
}
|
|
12364
12505
|
if (state.hasLocalWorkspace && (surface === "setup" || surface === "orient" && isEarly)) {
|
|
12365
12506
|
lines.push(COACH_ROOT_DIR);
|
|
12366
12507
|
}
|
|
@@ -12500,7 +12641,7 @@ function savePrepareContextFile(projectId, callerKey, content) {
|
|
|
12500
12641
|
var planPrepareCache = new PerCallerCache();
|
|
12501
12642
|
var planTool = {
|
|
12502
12643
|
name: "plan",
|
|
12503
|
-
description: '
|
|
12644
|
+
description: 'Turn a backlog into one scoped cycle of work, with a written spec for every task in it. plan reads the whole board, the decisions this project has already taken, and how much it actually delivered in recent cycles, then prioritises what to do next and writes a per-task BUILD HANDOFF: scope, what is deliberately out of scope, acceptance criteria, files likely touched, security notes, and the shared branch each task belongs on. It also reports board health, so stale, blocked and drifting work is visible instead of accumulating. This is not the same as planning inside one session: the cycle, the specs and the reasoning are stored, so the next session, the next tool, or the next person picks up where this one stopped. Run once per cycle, after setup the first time, or after completing all builds AND running release for the previous cycle. NEVER call when unbuilt cycle tasks exist: build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs, and generate handoffs separately via `handoff_generate`.',
|
|
12504
12645
|
annotations: { title: "Plan Cycle", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
12505
12646
|
inputSchema: {
|
|
12506
12647
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -17395,6 +17536,33 @@ async function ensureDesignHookRegistered(projectRoot) {
|
|
|
17395
17536
|
var TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
17396
17537
|
var CONVENTIONS_SENTINEL = "<!-- PAPI_CONVENTIONS -->";
|
|
17397
17538
|
var CONVENTIONS_HEADING = "## Code Style Conventions";
|
|
17539
|
+
function describeSeedWriteFailure(err) {
|
|
17540
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
17541
|
+
const CLASSES = [
|
|
17542
|
+
[
|
|
17543
|
+
/\b(?:401|403)\b|permission|unauthor|forbidden/i,
|
|
17544
|
+
"the storage backend refused the write (permission denied)."
|
|
17545
|
+
],
|
|
17546
|
+
[
|
|
17547
|
+
/\b(?:5\d\d)\b|internal server error|bad gateway|unavailable/i,
|
|
17548
|
+
"the storage backend returned a server error."
|
|
17549
|
+
],
|
|
17550
|
+
[
|
|
17551
|
+
/timeout|timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|fetch failed|network/i,
|
|
17552
|
+
"PAPI could not reach the storage backend."
|
|
17553
|
+
],
|
|
17554
|
+
[
|
|
17555
|
+
/duplicate key|unique constraint|23505/i,
|
|
17556
|
+
"a decision with that id already exists."
|
|
17557
|
+
],
|
|
17558
|
+
[
|
|
17559
|
+
/violates|constraint|invalid input syntax|22P02|23\d{3}/i,
|
|
17560
|
+
"the storage backend rejected the decision record."
|
|
17561
|
+
]
|
|
17562
|
+
];
|
|
17563
|
+
const matched = CLASSES.find(([pattern]) => pattern.test(raw));
|
|
17564
|
+
return matched ? `could not be saved \u2014 ${matched[1]}` : "could not be saved \u2014 the write to the storage backend failed.";
|
|
17565
|
+
}
|
|
17398
17566
|
async function applySetupOutputs(adapter2, config2, input, collector, briefText, adSeedText, conventionsText) {
|
|
17399
17567
|
const warnings = [];
|
|
17400
17568
|
await adapter2.updateProductBrief(briefText);
|
|
@@ -17434,9 +17602,19 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
17434
17602
|
let seededAds = 0;
|
|
17435
17603
|
let skippedAds = 0;
|
|
17436
17604
|
if (adSeedText) {
|
|
17605
|
+
let ads;
|
|
17606
|
+
let adSeedFailed = false;
|
|
17437
17607
|
try {
|
|
17438
17608
|
const cleaned = adSeedText.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
17439
|
-
|
|
17609
|
+
ads = JSON.parse(cleaned);
|
|
17610
|
+
} catch (err) {
|
|
17611
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17612
|
+
warnings.push(
|
|
17613
|
+
`AD seeding failed \u2014 active decisions were not created. Check that your ad_seed_response is valid JSON. Error: ${msg}`
|
|
17614
|
+
);
|
|
17615
|
+
adSeedFailed = true;
|
|
17616
|
+
}
|
|
17617
|
+
try {
|
|
17440
17618
|
if (Array.isArray(ads)) {
|
|
17441
17619
|
const existingAdIds = adapter2.getActiveDecisions ? new Set(
|
|
17442
17620
|
(await adapter2.getActiveDecisions({ includeRetired: true }).catch(() => [])).map((a) => a.displayId)
|
|
@@ -17460,18 +17638,17 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
17460
17638
|
}
|
|
17461
17639
|
}
|
|
17462
17640
|
} catch (err) {
|
|
17463
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
17464
17641
|
warnings.push(
|
|
17465
|
-
`
|
|
17642
|
+
`Active Decisions: ${describeSeedWriteFailure(err)}` + (seededAds > 0 ? ` ${seededAds} decision(s) were created before this and are saved \u2014 re-running \`setup\` will not duplicate them.` : " No decisions were created.") + " This is a PAPI-side failure, not a problem with your response."
|
|
17466
17643
|
);
|
|
17467
|
-
|
|
17644
|
+
adSeedFailed = true;
|
|
17468
17645
|
}
|
|
17469
17646
|
if (skippedAds > 0) {
|
|
17470
17647
|
warnings.push(
|
|
17471
17648
|
`Active Decisions: detected ${skippedAds} existing AD(s) and left them untouched${seededAds > 0 ? `; created ${seededAds} new one(s)` : " (none new to create)"}.`
|
|
17472
17649
|
);
|
|
17473
17650
|
} else if (seededAds === 0 && adSeedText) {
|
|
17474
|
-
if (!
|
|
17651
|
+
if (!adSeedFailed) {
|
|
17475
17652
|
warnings.push(
|
|
17476
17653
|
"AD seeding produced 0 active decisions \u2014 the JSON may be valid but empty or missing required `id` and `body` fields."
|
|
17477
17654
|
);
|
|
@@ -17532,6 +17709,61 @@ function isSecretFile(name) {
|
|
|
17532
17709
|
const lower = name.toLowerCase();
|
|
17533
17710
|
return SECRET_PATTERNS.some((p) => lower.includes(p));
|
|
17534
17711
|
}
|
|
17712
|
+
var SECRET_VALUE_PATTERNS = [
|
|
17713
|
+
// KEY=value / KEY: value where the KEY names a credential. Value must be
|
|
17714
|
+
// non-trivial so `API_KEY=` or `TOKEN=changeme` placeholders in docs are hit too.
|
|
17715
|
+
/\b([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_KEY|API_KEY|APIKEY|AUTH)[A-Z0-9_]*)\s*[:=]\s*\S+/gi,
|
|
17716
|
+
// Provider-issued key formats.
|
|
17717
|
+
/\bsk-[A-Za-z0-9_-]{16,}/g,
|
|
17718
|
+
// OpenAI / Anthropic style
|
|
17719
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
|
|
17720
|
+
// GitHub tokens
|
|
17721
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
17722
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
17723
|
+
// AWS access key id
|
|
17724
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
17725
|
+
// Slack
|
|
17726
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
17727
|
+
// JWT
|
|
17728
|
+
// Credentials embedded in a connection string.
|
|
17729
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s@]+@/gi,
|
|
17730
|
+
// Whole PEM blocks.
|
|
17731
|
+
/-----BEGIN[^-]*PRIVATE KEY-----[\s\S]*?-----END[^-]*PRIVATE KEY-----/g,
|
|
17732
|
+
/\bBearer\s+[A-Za-z0-9._-]{20,}/g
|
|
17733
|
+
];
|
|
17734
|
+
var REDACTED = "[redacted by PAPI]";
|
|
17735
|
+
function redactPackageJson(pkg) {
|
|
17736
|
+
if (!pkg) return pkg;
|
|
17737
|
+
const out = { ...pkg };
|
|
17738
|
+
for (const field of ["description", "name"]) {
|
|
17739
|
+
if (typeof out[field] === "string") out[field] = redactSecretValues(out[field]);
|
|
17740
|
+
}
|
|
17741
|
+
return out;
|
|
17742
|
+
}
|
|
17743
|
+
function normaliseClientScan(raw) {
|
|
17744
|
+
return {
|
|
17745
|
+
topLevelDirs: raw.topLevelDirs ?? [],
|
|
17746
|
+
topLevelFiles: (raw.topLevelFiles ?? []).filter((f) => !isSecretFile(f)),
|
|
17747
|
+
packageJson: redactPackageJson(raw.packageJson),
|
|
17748
|
+
readme: redactSecretValues(raw.readme),
|
|
17749
|
+
configFiles: (raw.configFiles ?? []).filter((f) => !isSecretFile(f)),
|
|
17750
|
+
sourceFiles: raw.sourceFiles ?? [],
|
|
17751
|
+
totalFiles: raw.totalFiles ?? 0,
|
|
17752
|
+
sourcePaths: (raw.sourcePaths ?? []).filter((p) => !isSecretFile(p)),
|
|
17753
|
+
commitHistory: raw.commitHistory?.map((c) => redactSecretValues(c))
|
|
17754
|
+
};
|
|
17755
|
+
}
|
|
17756
|
+
function redactSecretValues(text) {
|
|
17757
|
+
if (!text) return text;
|
|
17758
|
+
let out = text;
|
|
17759
|
+
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
17760
|
+
out = out.replace(pattern, (match) => {
|
|
17761
|
+
const kv = match.match(/^([A-Z0-9_]+)\s*[:=]/i);
|
|
17762
|
+
return kv ? `${kv[1]}=${REDACTED}` : REDACTED;
|
|
17763
|
+
});
|
|
17764
|
+
}
|
|
17765
|
+
return out;
|
|
17766
|
+
}
|
|
17535
17767
|
async function safeReadFile(filePath, maxBytes = 1e4) {
|
|
17536
17768
|
if (isSecretFile(basename(filePath))) return null;
|
|
17537
17769
|
try {
|
|
@@ -17721,18 +17953,7 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
17721
17953
|
const hasUserSignalForScan = Boolean(input.description?.trim()) || Boolean(input.targetUsers?.trim());
|
|
17722
17954
|
const shouldScan = isExistingProject || !hasUserSignalForScan;
|
|
17723
17955
|
if (input.codebaseScan) {
|
|
17724
|
-
const
|
|
17725
|
-
const clientScan = {
|
|
17726
|
-
topLevelDirs: raw.topLevelDirs ?? [],
|
|
17727
|
-
topLevelFiles: raw.topLevelFiles ?? [],
|
|
17728
|
-
packageJson: raw.packageJson,
|
|
17729
|
-
readme: raw.readme,
|
|
17730
|
-
configFiles: raw.configFiles ?? [],
|
|
17731
|
-
sourceFiles: raw.sourceFiles ?? [],
|
|
17732
|
-
totalFiles: raw.totalFiles ?? 0,
|
|
17733
|
-
sourcePaths: raw.sourcePaths ?? [],
|
|
17734
|
-
commitHistory: raw.commitHistory
|
|
17735
|
-
};
|
|
17956
|
+
const clientScan = normaliseClientScan(input.codebaseScan);
|
|
17736
17957
|
const hasScanSignal = Boolean(clientScan.readme) || Boolean(clientScan.packageJson) || Boolean(clientScan.commitHistory && clientScan.commitHistory.length > 0) || clientScan.topLevelDirs.length > 0 || clientScan.topLevelFiles.length > 0;
|
|
17737
17958
|
if (hasScanSignal) {
|
|
17738
17959
|
codebaseSummary = formatCodebaseSummary(clientScan, sourceContents);
|
|
@@ -17747,7 +17968,10 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
17747
17968
|
}
|
|
17748
17969
|
} else if (shouldScan && !canScanFilesystem && input.existingProject === true) {
|
|
17749
17970
|
warnings.push(
|
|
17750
|
-
|
|
17971
|
+
// task-3293: this used to end at "cannot read your local files", which read as
|
|
17972
|
+
// a flat contradiction to the very next thing the user saw — their own agent
|
|
17973
|
+
// reading those files. Name the division of labour instead of just the limit.
|
|
17974
|
+
"Codebase scan skipped \u2014 PAPI has no access to your machine and never reads your files directly. Your AI client can: ask it to scan the project and pass the summary back as `codebase_scan` (leaving out `.env` and anything holding keys, tokens or passwords). Or provide `description` and `target_users` directly and the brief generator will use those instead."
|
|
17751
17975
|
);
|
|
17752
17976
|
}
|
|
17753
17977
|
const hasCodebaseSignal = Boolean(codebaseSummary && codebaseSummary.trim().length > 0);
|
|
@@ -18077,7 +18301,7 @@ async function ensureMcpJsonGitignored(projectRoot) {
|
|
|
18077
18301
|
// src/tools/setup.ts
|
|
18078
18302
|
var setupTool = {
|
|
18079
18303
|
name: "setup",
|
|
18080
|
-
description: '
|
|
18304
|
+
description: 'Give a project a memory, so every later session starts with its context instead of a blank slate. Setup reads the project, writes a Product Brief that says what it is for and who it serves, records the first Active Decisions and build conventions so settled answers outlive the session that found them, and installs the workflow instructions the coding assistant follows from then on. Everything after this (planning, building, reviewing) reads from what setup writes. It takes a few minutes and runs in two passes: the first returns prompts for you to execute, the second saves the results. Tell the user what each pass is doing while it runs. Run after configuring your MCP credentials (via `init` or manually from getpapi.ai). Only project_name is required \u2014 description and target_users are derived from README, package.json, and commit history when omitted. Set existing_project: true to adopt an existing codebase. ADOPTING AN EXISTING PROJECT OVER A REMOTE/HOSTED CONNECTOR (no local stdio install): PAPI cannot read your filesystem, so YOU (the client) must gather a `codebase_scan` and pass it in \u2014 list top-level dirs/files, the package manifest, the README (first ~3000 chars), and recent commit subjects. Without it, adoption falls back to asking for description/target_users. On a local stdio install PAPI scans the tree itself, so `codebase_scan` is optional there. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run `plan` to start your first cycle.',
|
|
18081
18305
|
annotations: { title: "Set Up Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
18082
18306
|
inputSchema: {
|
|
18083
18307
|
type: "object",
|
|
@@ -18261,6 +18485,36 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
|
|
|
18261
18485
|
Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
|
|
18262
18486
|
);
|
|
18263
18487
|
}
|
|
18488
|
+
var SETUP_NARRATION_HEADING = "**What happens now, and how long it takes**";
|
|
18489
|
+
function formatSetupNarration(result) {
|
|
18490
|
+
const steps = [];
|
|
18491
|
+
if (result.preScanInstruction || result.newProjectInstruction) {
|
|
18492
|
+
steps.push("Gather the context below, so what follows describes this project rather than a generic one.");
|
|
18493
|
+
}
|
|
18494
|
+
if (result.briefPrompt) {
|
|
18495
|
+
steps.push("Write the Product Brief: what this project is, who it is for, and what it has to get right. Every later plan reads it.");
|
|
18496
|
+
}
|
|
18497
|
+
if (result.adSeedPrompt) {
|
|
18498
|
+
steps.push("Record the first Active Decisions, so the choices already made stop being re-argued in later sessions.");
|
|
18499
|
+
}
|
|
18500
|
+
if (result.conventionsPrompt) {
|
|
18501
|
+
steps.push("Capture the build conventions this project expects, so they reach every future build without being restated.");
|
|
18502
|
+
}
|
|
18503
|
+
if (result.northStarPrompt) {
|
|
18504
|
+
steps.push("Agree a North Star with the user, so progress can be judged against something.");
|
|
18505
|
+
}
|
|
18506
|
+
if (result.initialTasksPrompt) {
|
|
18507
|
+
steps.push("Seed the backlog, so the first planning cycle has real work to choose from.");
|
|
18508
|
+
}
|
|
18509
|
+
steps.push('Call `setup` again with `mode: "apply"` to save all of it.');
|
|
18510
|
+
return [
|
|
18511
|
+
SETUP_NARRATION_HEADING,
|
|
18512
|
+
"",
|
|
18513
|
+
"Expect a few minutes end to end. Nothing is saved until the final step, so do not stop partway.",
|
|
18514
|
+
"Say what you are doing as you go, in one line per step, so the wait is visible to the user:",
|
|
18515
|
+
...steps.map((s, i) => `${i + 1}. ${s}`)
|
|
18516
|
+
];
|
|
18517
|
+
}
|
|
18264
18518
|
async function handleSetup(adapter2, config2, args, clientName) {
|
|
18265
18519
|
const toolMode = args.mode;
|
|
18266
18520
|
const REQUIRED_FIELDS = ["project_name"];
|
|
@@ -18304,6 +18558,7 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
18304
18558
|
result.createdProject ? `Project "${result.projectName}" scaffolded \u2014 database tables created.
|
|
18305
18559
|
` : ""
|
|
18306
18560
|
];
|
|
18561
|
+
sections.push(...formatSetupNarration(result), "");
|
|
18307
18562
|
if (result.autoDetected) {
|
|
18308
18563
|
sections.push(
|
|
18309
18564
|
`**Codebase detected:** Existing codebase found \u2014 running in adoption mode. If this is wrong, re-run setup with \`existing_project: false\`.`,
|
|
@@ -20260,16 +20515,20 @@ function triggerSurfaceHitsOnBranch(projectRoot, baseRef = "origin/main") {
|
|
|
20260
20515
|
}
|
|
20261
20516
|
|
|
20262
20517
|
// src/services/build.ts
|
|
20263
|
-
function selectAutostashPaths(modified, untracked) {
|
|
20518
|
+
function selectAutostashPaths(modified, untracked, conflictingPaths) {
|
|
20264
20519
|
const untrackedSet = new Set(untracked);
|
|
20265
|
-
const isDocsPath = (p) => p === "docs" || p.startsWith("docs/");
|
|
20266
20520
|
const isUntrackedDirEntry = (p) => p.endsWith("/") && untracked.some((u) => u.startsWith(p));
|
|
20267
20521
|
const trackedDirty = modified.filter(
|
|
20268
20522
|
(p) => !untrackedSet.has(p) && !isUntrackedDirEntry(p)
|
|
20269
20523
|
);
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20524
|
+
if (conflictingPaths == null) {
|
|
20525
|
+
return { toStash: trackedDirty, preservedDocs: untracked };
|
|
20526
|
+
}
|
|
20527
|
+
const conflicting = new Set(conflictingPaths);
|
|
20528
|
+
return {
|
|
20529
|
+
toStash: trackedDirty.filter((p) => conflicting.has(p)),
|
|
20530
|
+
preservedDocs: untracked
|
|
20531
|
+
};
|
|
20273
20532
|
}
|
|
20274
20533
|
function resolveRelatedDecisions(provided, buildHandoff) {
|
|
20275
20534
|
const fromInput = (provided ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -20391,11 +20650,18 @@ function matchedPredictedEntry(changedPath, predicted) {
|
|
|
20391
20650
|
function isPathInPredictedScope(changedPath, predicted) {
|
|
20392
20651
|
return matchedPredictedEntry(changedPath, predicted) !== null;
|
|
20393
20652
|
}
|
|
20653
|
+
function verifyCommitContents(cwd, intended) {
|
|
20654
|
+
if (intended.length === 0) return [];
|
|
20655
|
+
const actual = getCommitFiles(cwd);
|
|
20656
|
+
if (actual === null) return [];
|
|
20657
|
+
const actualSet = new Set(actual);
|
|
20658
|
+
return intended.filter((p) => !actualSet.has(p));
|
|
20659
|
+
}
|
|
20394
20660
|
function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
20395
20661
|
const cwd = config2.projectRoot;
|
|
20396
20662
|
const message = `feat(${taskId}): ${taskTitle}`;
|
|
20397
|
-
if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
|
|
20398
|
-
if (!isGitRepo(cwd)) return "Auto-commit: skipped (not a git repository).";
|
|
20663
|
+
if (!isGitAvailable()) return { line: "Auto-commit: skipped (git not found).", missing: [] };
|
|
20664
|
+
if (!isGitRepo(cwd)) return { line: "Auto-commit: skipped (not a git repository).", missing: [] };
|
|
20399
20665
|
const safeRun = (fn) => {
|
|
20400
20666
|
try {
|
|
20401
20667
|
const r = fn();
|
|
@@ -20409,26 +20675,27 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
20409
20675
|
`^(feat|fix|chore|refactor|docs|test|style|build|perf|ci)\\(${taskId}\\):`
|
|
20410
20676
|
);
|
|
20411
20677
|
if (headSubject && taskCommitRe.test(headSubject)) {
|
|
20412
|
-
return `Auto-commit: skipped \u2014 HEAD already has a fresh ${taskId} commit (${headSubject})
|
|
20678
|
+
return { line: `Auto-commit: skipped \u2014 HEAD already has a fresh ${taskId} commit (${headSubject}).`, missing: [] };
|
|
20413
20679
|
}
|
|
20414
20680
|
const staged = getStagedFiles(cwd);
|
|
20415
20681
|
if (staged.length > 0) {
|
|
20416
|
-
|
|
20682
|
+
const line = safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
20683
|
+
return { line, missing: verifyCommitContents(cwd, staged) };
|
|
20417
20684
|
}
|
|
20418
20685
|
const checkpoint = readBuildCheckpointIfLocal({ cwd, taskId });
|
|
20419
20686
|
const headSha = getHeadCommitSha(cwd);
|
|
20420
20687
|
if (checkpoint?.lastCommitSha && headSha && checkpoint.lastCommitSha !== headSha) {
|
|
20421
20688
|
const leftover = getModifiedFiles(cwd);
|
|
20422
20689
|
if (leftover.length === 0) {
|
|
20423
|
-
return "Auto-commit: skipped (builder already committed; working tree clean).";
|
|
20690
|
+
return { line: "Auto-commit: skipped (builder already committed; working tree clean).", missing: [] };
|
|
20424
20691
|
}
|
|
20425
20692
|
const sample = leftover.slice(0, 10).join(", ");
|
|
20426
20693
|
const more = leftover.length > 10 ? ` (+${leftover.length - 10} more)` : "";
|
|
20427
|
-
return `Auto-commit: skipped \u2014 you already committed during this build, and ${leftover.length} file(s) are still modified. They were NOT committed, because at this point PAPI cannot tell your deliberately-excluded work from a concurrent session's files (task-3054). Left uncommitted: ${sample}${more}. If any belong to ${taskId}, \`git add\` them and re-run build_execute complete
|
|
20694
|
+
return { line: `Auto-commit: skipped \u2014 you already committed during this build, and ${leftover.length} file(s) are still modified. They were NOT committed, because at this point PAPI cannot tell your deliberately-excluded work from a concurrent session's files (task-3054). Left uncommitted: ${sample}${more}. If any belong to ${taskId}, \`git add\` them and re-run build_execute complete.`, missing: [] };
|
|
20428
20695
|
}
|
|
20429
20696
|
const modified = getModifiedFiles(cwd);
|
|
20430
20697
|
if (modified.length === 0) {
|
|
20431
|
-
return "Auto-commit: skipped (no working-tree changes).";
|
|
20698
|
+
return { line: "Auto-commit: skipped (no working-tree changes).", missing: [] };
|
|
20432
20699
|
}
|
|
20433
20700
|
const commitResult = safeRun(() => stageAllAndCommit(cwd, message));
|
|
20434
20701
|
if (predictedFiles && predictedFiles.length > 0) {
|
|
@@ -20437,13 +20704,22 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
20437
20704
|
if (outOfScope.length > 0) {
|
|
20438
20705
|
const sample = outOfScope.slice(0, 10).join(", ");
|
|
20439
20706
|
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
20440
|
-
return
|
|
20707
|
+
return {
|
|
20708
|
+
line: `${commitResult} (staged all ${modified.length} changed file(s)). \u2139\uFE0F Scope drift: ${outOfScope.length} committed file(s) were outside the handoff's FILES LIKELY TOUCHED \u2014 handoff under-predicted: ${sample}${more}.`,
|
|
20709
|
+
missing: verifyCommitContents(cwd, modified)
|
|
20710
|
+
};
|
|
20441
20711
|
}
|
|
20442
20712
|
const matches = modified.slice(0, 5).map((p) => `${p} \u2190 ${matchedPredictedEntry(p, cleanedPredicted) ?? "?"}`).join(", ");
|
|
20443
20713
|
const extra = modified.length > 5 ? ` (+${modified.length - 5} more)` : "";
|
|
20444
|
-
return
|
|
20714
|
+
return {
|
|
20715
|
+
line: `${commitResult} (staged all ${modified.length} changed file(s), each matched to FILES LIKELY TOUCHED: ${matches}${extra}).`,
|
|
20716
|
+
missing: verifyCommitContents(cwd, modified)
|
|
20717
|
+
};
|
|
20445
20718
|
}
|
|
20446
|
-
return
|
|
20719
|
+
return {
|
|
20720
|
+
line: `${commitResult} (staged all ${modified.length} changed file(s)).`,
|
|
20721
|
+
missing: verifyCommitContents(cwd, modified)
|
|
20722
|
+
};
|
|
20447
20723
|
}
|
|
20448
20724
|
function pushAndCreatePR(config2, taskId, taskTitle, clientName, module, cycleNumber) {
|
|
20449
20725
|
const lines = [];
|
|
@@ -20859,20 +21135,23 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20859
21135
|
);
|
|
20860
21136
|
}
|
|
20861
21137
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
21138
|
+
const stashTarget = branchExists(config2.projectRoot, featureBranch) ? featureBranch : resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
21139
|
+
const conflictingPaths = getPathsDifferingFrom(config2.projectRoot, stashTarget);
|
|
20862
21140
|
const { toStash, preservedDocs } = selectAutostashPaths(
|
|
20863
21141
|
getModifiedFiles(config2.projectRoot),
|
|
20864
|
-
getUntrackedFiles(config2.projectRoot)
|
|
21142
|
+
getUntrackedFiles(config2.projectRoot),
|
|
21143
|
+
conflictingPaths
|
|
20865
21144
|
);
|
|
20866
|
-
const preservedNote = preservedDocs.length > 0 ? ` ${preservedDocs.length} untracked
|
|
21145
|
+
const preservedNote = preservedDocs.length > 0 ? ` ${preservedDocs.length} untracked file(s) left in place, not stashed (task-3282 \u2014 an untracked file never blocks a branch switch, and a sub-agent's brand-new file is untracked by definition).` : "";
|
|
20867
21146
|
if (toStash.length === 0) {
|
|
20868
21147
|
if (preservedDocs.length > 0) {
|
|
20869
|
-
branchLines.push(`Auto-stash skipped \u2014
|
|
21148
|
+
branchLines.push(`Auto-stash skipped \u2014 nothing tracked is dirty.${preservedNote}`);
|
|
20870
21149
|
}
|
|
20871
21150
|
} else {
|
|
20872
21151
|
const stashLabel = `papi-autostash/${taskId}-${Math.floor(Date.now() / 1e3)}`;
|
|
20873
21152
|
try {
|
|
20874
21153
|
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
20875
|
-
execFileSync7("git", ["stash", "push", "-
|
|
21154
|
+
execFileSync7("git", ["stash", "push", "-m", stashLabel, "--", ...toStash], {
|
|
20876
21155
|
cwd: config2.projectRoot,
|
|
20877
21156
|
encoding: "utf-8"
|
|
20878
21157
|
});
|
|
@@ -21433,7 +21712,20 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
21433
21712
|
} else if (dbOnlyMode) {
|
|
21434
21713
|
commitLine = DB_ONLY_COMPLETE_NOTICE;
|
|
21435
21714
|
} else {
|
|
21436
|
-
|
|
21715
|
+
const outcome = autoCommit(config2, taskId, task.title, task.buildHandoff?.filesLikelyTouched);
|
|
21716
|
+
commitLine = outcome.line;
|
|
21717
|
+
if (outcome.missing.length > 0) {
|
|
21718
|
+
const sample = outcome.missing.slice(0, 10).join(", ");
|
|
21719
|
+
const more = outcome.missing.length > 10 ? ` (+${outcome.missing.length - 10} more)` : "";
|
|
21720
|
+
if (input.completed === "yes" && !atomicCommitDone) {
|
|
21721
|
+
try {
|
|
21722
|
+
await adapter2.updateTaskStatus(taskId, task.status);
|
|
21723
|
+
} catch {
|
|
21724
|
+
}
|
|
21725
|
+
}
|
|
21726
|
+
commitLine = `\u26A0\uFE0F AUTO-COMMIT VERIFICATION FAILED \u2014 the commit does NOT contain ${outcome.missing.length} file(s) that were staged for it. Missing: ${sample}${more}. This is the task-3029 failure mode: a pre-commit hook reverted modified files while staged adds/renames survived, so the commit is PARTIAL and will not build. ${taskId} was NOT advanced \u2014 it stays ${task.status}. Fix: inspect \`git show --stat HEAD\`, restore the missing changes, \`git add\` them, and re-run build_execute complete.
|
|
21727
|
+
(Original commit note: ${outcome.line})`;
|
|
21728
|
+
}
|
|
21437
21729
|
}
|
|
21438
21730
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
21439
21731
|
const sha = getHeadCommitSha(config2.projectRoot);
|
|
@@ -21603,6 +21895,216 @@ async function cancelBuild(adapter2, taskId, reason, projectRoot) {
|
|
|
21603
21895
|
return { task, reason };
|
|
21604
21896
|
}
|
|
21605
21897
|
|
|
21898
|
+
// src/tools/conventions.ts
|
|
21899
|
+
var MAX_CONVENTION_RULE_LENGTH = 2e3;
|
|
21900
|
+
var MAX_CONVENTION_MODULE_LENGTH = 120;
|
|
21901
|
+
var MAX_INJECTED_CONVENTIONS = 25;
|
|
21902
|
+
var MAX_INJECTED_CONVENTION_CHARS = 8e3;
|
|
21903
|
+
var CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
21904
|
+
function sanitiseConventionRule(raw) {
|
|
21905
|
+
if (typeof raw !== "string") return null;
|
|
21906
|
+
const stripped = raw.replace(CONTROL_CHARS, "").trim();
|
|
21907
|
+
if (!stripped) return null;
|
|
21908
|
+
return stripped.slice(0, MAX_CONVENTION_RULE_LENGTH);
|
|
21909
|
+
}
|
|
21910
|
+
function sanitiseConventionModule(raw) {
|
|
21911
|
+
if (typeof raw !== "string") return void 0;
|
|
21912
|
+
const stripped = raw.replace(CONTROL_CHARS, "").trim();
|
|
21913
|
+
if (!stripped) return void 0;
|
|
21914
|
+
return stripped.slice(0, MAX_CONVENTION_MODULE_LENGTH);
|
|
21915
|
+
}
|
|
21916
|
+
function selectConventionsForModule(conventions, module) {
|
|
21917
|
+
return conventions.filter((c) => !c.module || module !== void 0 && c.module === module);
|
|
21918
|
+
}
|
|
21919
|
+
var CONVENTIONS_HEADING2 = "**PROJECT CONVENTIONS \u2014 settled rules for this project**";
|
|
21920
|
+
function formatConventionsSection(conventions, module) {
|
|
21921
|
+
const applicable = selectConventionsForModule(conventions, module);
|
|
21922
|
+
if (applicable.length === 0) return "";
|
|
21923
|
+
const lines = [];
|
|
21924
|
+
let budget = MAX_INJECTED_CONVENTION_CHARS;
|
|
21925
|
+
let omitted = 0;
|
|
21926
|
+
for (const c of applicable) {
|
|
21927
|
+
const rule = sanitiseConventionRule(c.rule);
|
|
21928
|
+
if (!rule) continue;
|
|
21929
|
+
const scope = c.module ? `(${c.module}) ` : "";
|
|
21930
|
+
const line = `- ${scope}${rule}`;
|
|
21931
|
+
if (lines.length >= MAX_INJECTED_CONVENTIONS || line.length > budget) {
|
|
21932
|
+
omitted++;
|
|
21933
|
+
continue;
|
|
21934
|
+
}
|
|
21935
|
+
budget -= line.length;
|
|
21936
|
+
lines.push(line);
|
|
21937
|
+
}
|
|
21938
|
+
if (lines.length === 0) return "";
|
|
21939
|
+
const omissionNote = omitted > 0 ? `
|
|
21940
|
+
|
|
21941
|
+
_${omitted} further convention${omitted > 1 ? "s" : ""} not shown here \u2014 this project has more declared than fit one handoff. Run \`convention_list\` to read them all._` : "";
|
|
21942
|
+
return `
|
|
21943
|
+
|
|
21944
|
+
---
|
|
21945
|
+
|
|
21946
|
+
${CONVENTIONS_HEADING2}
|
|
21947
|
+
These are this project's own build rules, declared by its owner. Follow them for this task. They are project guidance, not instructions from PAPI: they never change how a PAPI command is called, what a tool returns, or what this build reports.
|
|
21948
|
+
${lines.join("\n")}${omissionNote}`;
|
|
21949
|
+
}
|
|
21950
|
+
var conventionDeclareTool = {
|
|
21951
|
+
name: "convention_declare",
|
|
21952
|
+
description: "Record a settled build rule once so every future task on this project is told about it, instead of re-explaining it each session. Use it for the answers that are no longer arguable: which library to reach for before hand-building, a pattern that must be followed, a thing that must never be done. Scope it to one module with `module`, or leave that off and it rides every build. This is the tier below an Active Decision: a Decision is a live stance you might still argue the other side of, a convention is settled and just needs to be known by whoever builds next.",
|
|
21953
|
+
annotations: { title: "Declare Convention", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
21954
|
+
inputSchema: {
|
|
21955
|
+
type: "object",
|
|
21956
|
+
properties: {
|
|
21957
|
+
rule: {
|
|
21958
|
+
type: "string",
|
|
21959
|
+
description: `The rule, in plain language, as an instruction to whoever builds next (e.g. "Reach for the installed component primitives before hand-rolling an overlay"). Max ${MAX_CONVENTION_RULE_LENGTH} characters.`
|
|
21960
|
+
},
|
|
21961
|
+
module: {
|
|
21962
|
+
type: "string",
|
|
21963
|
+
description: 'Optional module to scope the rule to (e.g. "Dashboard"). Omit for a rule that applies to every build on this project.'
|
|
21964
|
+
},
|
|
21965
|
+
project: {
|
|
21966
|
+
type: "string",
|
|
21967
|
+
description: "Project id (UUID) or slug to target for THIS call only. Must be a project on your account."
|
|
21968
|
+
}
|
|
21969
|
+
},
|
|
21970
|
+
required: ["rule"]
|
|
21971
|
+
}
|
|
21972
|
+
};
|
|
21973
|
+
var conventionListTool = {
|
|
21974
|
+
name: "convention_list",
|
|
21975
|
+
description: "List the build rules this project has declared, with the id needed to remove one. Shows which rules ride every build and which are scoped to a single module.",
|
|
21976
|
+
annotations: { title: "List Conventions", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
21977
|
+
inputSchema: {
|
|
21978
|
+
type: "object",
|
|
21979
|
+
properties: {
|
|
21980
|
+
module: {
|
|
21981
|
+
type: "string",
|
|
21982
|
+
description: "Optional: show only the rules that would apply to a build in this module (project-wide rules plus that module's own)."
|
|
21983
|
+
},
|
|
21984
|
+
project: {
|
|
21985
|
+
type: "string",
|
|
21986
|
+
description: "Project id (UUID) or slug to target for THIS call only. Must be a project on your account."
|
|
21987
|
+
}
|
|
21988
|
+
},
|
|
21989
|
+
required: []
|
|
21990
|
+
}
|
|
21991
|
+
};
|
|
21992
|
+
var conventionRemoveTool = {
|
|
21993
|
+
name: "convention_remove",
|
|
21994
|
+
description: "Remove a declared build rule so it stops appearing in future builds. Takes the id from `convention_list`. Existing handoffs already generated are unaffected; the next build no longer carries it.",
|
|
21995
|
+
annotations: { title: "Remove Convention", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
21996
|
+
inputSchema: {
|
|
21997
|
+
type: "object",
|
|
21998
|
+
properties: {
|
|
21999
|
+
id: {
|
|
22000
|
+
type: "string",
|
|
22001
|
+
description: "The convention id, from `convention_list`."
|
|
22002
|
+
},
|
|
22003
|
+
project: {
|
|
22004
|
+
type: "string",
|
|
22005
|
+
description: "Project id (UUID) or slug to target for THIS call only. Must be a project on your account."
|
|
22006
|
+
}
|
|
22007
|
+
},
|
|
22008
|
+
required: ["id"]
|
|
22009
|
+
}
|
|
22010
|
+
};
|
|
22011
|
+
var UNSUPPORTED = "Conventions are not available on this connection. The store lives in the PAPI database, so a project running on a file-backed adapter has nowhere to record one.";
|
|
22012
|
+
async function handleConventionDeclare(adapter2, config2, args) {
|
|
22013
|
+
const tracker = new ProgressTracker("validate");
|
|
22014
|
+
try {
|
|
22015
|
+
if (!adapter2.createConvention) return errorResponse(UNSUPPORTED);
|
|
22016
|
+
const rule = sanitiseConventionRule(args.rule);
|
|
22017
|
+
if (!rule) {
|
|
22018
|
+
return errorResponse(
|
|
22019
|
+
"A convention needs a rule. Pass `rule` with the instruction you want every future build on this project to follow."
|
|
22020
|
+
);
|
|
22021
|
+
}
|
|
22022
|
+
const wasTruncated = typeof args.rule === "string" && args.rule.trim().length > MAX_CONVENTION_RULE_LENGTH;
|
|
22023
|
+
const moduleScope = sanitiseConventionModule(args.module);
|
|
22024
|
+
tracker.mark("write");
|
|
22025
|
+
const created = await adapter2.createConvention({ rule, ...moduleScope ? { module: moduleScope } : {} });
|
|
22026
|
+
const scopeNote = moduleScope ? `It rides every build in the **${moduleScope}** module.` : "It rides every build on this project.";
|
|
22027
|
+
const truncationNote = wasTruncated ? `
|
|
22028
|
+
|
|
22029
|
+
\u26A0\uFE0F The rule was longer than ${MAX_CONVENTION_RULE_LENGTH} characters and was shortened to fit. Re-declare it shorter if the trimmed version reads wrong.` : "";
|
|
22030
|
+
return textResponse(
|
|
22031
|
+
`Convention recorded. ${scopeNote}
|
|
22032
|
+
|
|
22033
|
+
> ${rule}
|
|
22034
|
+
|
|
22035
|
+
Id: \`${created.id ?? "(unknown)"}\` \u2014 remove it with \`convention_remove\`.${truncationNote}`
|
|
22036
|
+
);
|
|
22037
|
+
} catch (err) {
|
|
22038
|
+
return errorResponse(formatStructuredError({
|
|
22039
|
+
tool: "convention_declare",
|
|
22040
|
+
mode: "declare",
|
|
22041
|
+
adapter: config2.adapterType,
|
|
22042
|
+
lastStep: tracker.lastStep,
|
|
22043
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22044
|
+
hint: defaultHint("convention_declare")
|
|
22045
|
+
}));
|
|
22046
|
+
}
|
|
22047
|
+
}
|
|
22048
|
+
async function handleConventionList(adapter2, config2, args) {
|
|
22049
|
+
const tracker = new ProgressTracker("read");
|
|
22050
|
+
try {
|
|
22051
|
+
if (!adapter2.listConventions) return errorResponse(UNSUPPORTED);
|
|
22052
|
+
const all = await adapter2.listConventions();
|
|
22053
|
+
const moduleFilter = sanitiseConventionModule(args.module);
|
|
22054
|
+
const rows = moduleFilter ? selectConventionsForModule(all, moduleFilter) : all;
|
|
22055
|
+
if (rows.length === 0) {
|
|
22056
|
+
return textResponse(
|
|
22057
|
+
moduleFilter ? `No conventions apply to the **${moduleFilter}** module yet. Declare one with \`convention_declare\` and every future build in it will carry the rule.` : "No conventions declared yet. When you settle a question you do not want to answer again, record it with `convention_declare` and every future build will carry it."
|
|
22058
|
+
);
|
|
22059
|
+
}
|
|
22060
|
+
const lines = rows.map((c) => {
|
|
22061
|
+
const scope = c.module ? `**${c.module}**` : "every build";
|
|
22062
|
+
return `- ${scope} \u2014 ${sanitiseConventionRule(c.rule) ?? c.rule}
|
|
22063
|
+
\`${c.id ?? "(no id)"}\``;
|
|
22064
|
+
});
|
|
22065
|
+
const header = moduleFilter ? `${rows.length} convention${rows.length > 1 ? "s" : ""} apply to a build in **${moduleFilter}** (project-wide rules included):` : `${rows.length} convention${rows.length > 1 ? "s" : ""} declared on this project:`;
|
|
22066
|
+
return textResponse(`${header}
|
|
22067
|
+
|
|
22068
|
+
${lines.join("\n")}`);
|
|
22069
|
+
} catch (err) {
|
|
22070
|
+
return errorResponse(formatStructuredError({
|
|
22071
|
+
tool: "convention_list",
|
|
22072
|
+
mode: "list",
|
|
22073
|
+
adapter: config2.adapterType,
|
|
22074
|
+
lastStep: tracker.lastStep,
|
|
22075
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22076
|
+
hint: defaultHint("convention_list")
|
|
22077
|
+
}));
|
|
22078
|
+
}
|
|
22079
|
+
}
|
|
22080
|
+
async function handleConventionRemove(adapter2, config2, args) {
|
|
22081
|
+
const tracker = new ProgressTracker("validate");
|
|
22082
|
+
try {
|
|
22083
|
+
if (!adapter2.deleteConvention) return errorResponse(UNSUPPORTED);
|
|
22084
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
22085
|
+
if (!id) {
|
|
22086
|
+
return errorResponse("Pass the `id` of the convention to remove. `convention_list` prints the id under each rule.");
|
|
22087
|
+
}
|
|
22088
|
+
tracker.mark("delete");
|
|
22089
|
+
const removed = await adapter2.deleteConvention(id);
|
|
22090
|
+
if (!removed) {
|
|
22091
|
+
return errorResponse(
|
|
22092
|
+
`No convention with id \`${id}\` on this project. Run \`convention_list\` to see what is declared.`
|
|
22093
|
+
);
|
|
22094
|
+
}
|
|
22095
|
+
return textResponse(`Convention removed. Future builds no longer carry it.`);
|
|
22096
|
+
} catch (err) {
|
|
22097
|
+
return errorResponse(formatStructuredError({
|
|
22098
|
+
tool: "convention_remove",
|
|
22099
|
+
mode: "remove",
|
|
22100
|
+
adapter: config2.adapterType,
|
|
22101
|
+
lastStep: tracker.lastStep,
|
|
22102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22103
|
+
hint: defaultHint("convention_remove")
|
|
22104
|
+
}));
|
|
22105
|
+
}
|
|
22106
|
+
}
|
|
22107
|
+
|
|
21606
22108
|
// src/tools/module-instructions.ts
|
|
21607
22109
|
var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
|
|
21608
22110
|
var MODULE_INSTRUCTIONS = {
|
|
@@ -21681,6 +22183,194 @@ function getModuleInstructions(module) {
|
|
|
21681
22183
|
|
|
21682
22184
|
${instructions}`;
|
|
21683
22185
|
}
|
|
22186
|
+
function getBuilderInstructions(module, conventions) {
|
|
22187
|
+
const conventionsSection = formatConventionsSection(conventions, module);
|
|
22188
|
+
const hasModuleScopedOverride = Boolean(
|
|
22189
|
+
module && conventions.some((c) => c.module === module)
|
|
22190
|
+
);
|
|
22191
|
+
const builtIn = hasModuleScopedOverride ? "" : getModuleInstructions(module);
|
|
22192
|
+
return builtIn + conventionsSection;
|
|
22193
|
+
}
|
|
22194
|
+
|
|
22195
|
+
// src/services/build-proposal.ts
|
|
22196
|
+
var MAX_PROPOSAL_TITLE = 200;
|
|
22197
|
+
var MAX_PROPOSAL_BODY = 2e3;
|
|
22198
|
+
function validateProposal(raw) {
|
|
22199
|
+
if (typeof raw !== "object" || raw === null) {
|
|
22200
|
+
return { error: "proposal must be an object." };
|
|
22201
|
+
}
|
|
22202
|
+
const p = raw;
|
|
22203
|
+
if (typeof p.title === "string" && p.title.trim().length > MAX_PROPOSAL_TITLE) {
|
|
22204
|
+
return { error: `proposal.title exceeds ${MAX_PROPOSAL_TITLE} characters.` };
|
|
22205
|
+
}
|
|
22206
|
+
if (typeof p.body === "string" && p.body.trim().length > MAX_PROPOSAL_BODY) {
|
|
22207
|
+
return { error: `proposal.body exceeds ${MAX_PROPOSAL_BODY} characters.` };
|
|
22208
|
+
}
|
|
22209
|
+
const title = sanitiseConventionRule(p.title) ?? "";
|
|
22210
|
+
if (!title) return { error: "proposal.title is required \u2014 one line stating the stance or rule." };
|
|
22211
|
+
const body = sanitiseConventionRule(p.body) ?? "";
|
|
22212
|
+
if (!body) return { error: "proposal.body is required \u2014 what was decided or settled, and why." };
|
|
22213
|
+
const t = p.tests;
|
|
22214
|
+
if (typeof t !== "object" || t === null) {
|
|
22215
|
+
return { error: "proposal.tests is required \u2014 apply the four admission tests and report each as a boolean." };
|
|
22216
|
+
}
|
|
22217
|
+
const tt = t;
|
|
22218
|
+
const keys = [
|
|
22219
|
+
"alternativesWereReal",
|
|
22220
|
+
"constrainsFutureWork",
|
|
22221
|
+
"arguableToday",
|
|
22222
|
+
"reversalCostsMore"
|
|
22223
|
+
];
|
|
22224
|
+
for (const k of keys) {
|
|
22225
|
+
if (typeof tt[k] !== "boolean") {
|
|
22226
|
+
return { error: `proposal.tests.${k} must be true or false. All four admission tests must be answered explicitly \u2014 an omitted test is not a failed test.` };
|
|
22227
|
+
}
|
|
22228
|
+
}
|
|
22229
|
+
const moduleScope = sanitiseConventionModule(p.module);
|
|
22230
|
+
return {
|
|
22231
|
+
proposal: {
|
|
22232
|
+
title,
|
|
22233
|
+
body,
|
|
22234
|
+
...moduleScope ? { module: moduleScope } : {},
|
|
22235
|
+
tests: {
|
|
22236
|
+
alternativesWereReal: tt.alternativesWereReal,
|
|
22237
|
+
constrainsFutureWork: tt.constrainsFutureWork,
|
|
22238
|
+
arguableToday: tt.arguableToday,
|
|
22239
|
+
reversalCostsMore: tt.reversalCostsMore
|
|
22240
|
+
}
|
|
22241
|
+
}
|
|
22242
|
+
};
|
|
22243
|
+
}
|
|
22244
|
+
function routeProposal(proposal) {
|
|
22245
|
+
const { alternativesWereReal, constrainsFutureWork, arguableToday, reversalCostsMore } = proposal.tests;
|
|
22246
|
+
if (!constrainsFutureWork) {
|
|
22247
|
+
return {
|
|
22248
|
+
outcome: "rejected",
|
|
22249
|
+
failedTest: "b",
|
|
22250
|
+
reason: "It does not constrain future work, so it is not durable \u2014 it is just work. Capture it as a task, a build report note, or a doc."
|
|
22251
|
+
};
|
|
22252
|
+
}
|
|
22253
|
+
if (!alternativesWereReal) {
|
|
22254
|
+
return {
|
|
22255
|
+
outcome: "rejected",
|
|
22256
|
+
failedTest: "a",
|
|
22257
|
+
reason: "There were no real alternatives, so no stance was taken. If there was only ever one way to do it, it is not a decision. Capture it as a task, a note, or a doc."
|
|
22258
|
+
};
|
|
22259
|
+
}
|
|
22260
|
+
if (!reversalCostsMore) {
|
|
22261
|
+
return {
|
|
22262
|
+
outcome: "rejected",
|
|
22263
|
+
failedTest: "d",
|
|
22264
|
+
reason: "Reversing it is as cheap as doing it, so nothing is being constrained. Capture it as a task, a note, or a doc."
|
|
22265
|
+
};
|
|
22266
|
+
}
|
|
22267
|
+
if (!arguableToday) {
|
|
22268
|
+
return {
|
|
22269
|
+
outcome: "recorded-convention",
|
|
22270
|
+
failedTest: "c",
|
|
22271
|
+
reason: "Real alternatives, binds future work, expensive to reverse, but nobody is arguing the other side any more. That is a Convention, not a Decision \u2014 it does not need adjudicating, it needs to be known by whoever builds next."
|
|
22272
|
+
};
|
|
22273
|
+
}
|
|
22274
|
+
return {
|
|
22275
|
+
outcome: "queued-decision",
|
|
22276
|
+
reason: "All four admission tests pass, so this is a live stance. Queued for your decision \u2014 nothing has been minted."
|
|
22277
|
+
};
|
|
22278
|
+
}
|
|
22279
|
+
var PROPOSED_DECISION_REF = "proposed-decision";
|
|
22280
|
+
async function applyProposal(adapter2, routing, proposal, ctx) {
|
|
22281
|
+
try {
|
|
22282
|
+
if (routing.outcome === "rejected") return {};
|
|
22283
|
+
if (routing.outcome === "recorded-convention") {
|
|
22284
|
+
if (!adapter2.createConvention) {
|
|
22285
|
+
return { error: "This connection has nowhere to record a convention." };
|
|
22286
|
+
}
|
|
22287
|
+
const rule = `${proposal.title}. ${proposal.body}`.slice(0, 2e3);
|
|
22288
|
+
const convention = await adapter2.createConvention({
|
|
22289
|
+
rule,
|
|
22290
|
+
...proposal.module ? { module: proposal.module } : {}
|
|
22291
|
+
});
|
|
22292
|
+
return { convention };
|
|
22293
|
+
}
|
|
22294
|
+
if (!adapter2.createTask || !adapter2.updateTask) {
|
|
22295
|
+
return { error: "This connection cannot queue a decision for confirmation." };
|
|
22296
|
+
}
|
|
22297
|
+
const provenance = ctx.sourceTaskId ? `Proposed while building ${ctx.sourceTaskId}.` : "Proposed during a build.";
|
|
22298
|
+
const task = await adapter2.createTask({
|
|
22299
|
+
displayId: "",
|
|
22300
|
+
title: `Decision proposed: ${proposal.title}`,
|
|
22301
|
+
status: "Backlog",
|
|
22302
|
+
priority: "P1 High",
|
|
22303
|
+
complexity: "XS",
|
|
22304
|
+
module: proposal.module || "Core",
|
|
22305
|
+
epic: "Platform",
|
|
22306
|
+
phase: "Unscoped",
|
|
22307
|
+
owner: "TBD",
|
|
22308
|
+
reviewed: false,
|
|
22309
|
+
createdCycle: ctx.cycleNumber,
|
|
22310
|
+
taskType: "task",
|
|
22311
|
+
notes: `${provenance} All four admission tests passed, so this is a live stance rather than a settled rule.
|
|
22312
|
+
|
|
22313
|
+
${proposal.body}
|
|
22314
|
+
|
|
22315
|
+
Accept it by minting the decision with \`strategy_change\`, or dismiss it by cancelling this task. Nothing has been written as an Active Decision.`
|
|
22316
|
+
});
|
|
22317
|
+
await adapter2.updateTask(task.id, {
|
|
22318
|
+
status: "Blocked",
|
|
22319
|
+
blocker: {
|
|
22320
|
+
type: "decision-gate",
|
|
22321
|
+
ref: PROPOSED_DECISION_REF,
|
|
22322
|
+
reason: "A build proposed this decision. Awaiting the owner's call \u2014 it does not auto-clear, because only a person can rule on it.",
|
|
22323
|
+
blockedCycle: ctx.cycleNumber
|
|
22324
|
+
}
|
|
22325
|
+
});
|
|
22326
|
+
return { taskId: task.id };
|
|
22327
|
+
} catch (err) {
|
|
22328
|
+
return { error: err instanceof Error ? err.message : "Unknown error." };
|
|
22329
|
+
}
|
|
22330
|
+
}
|
|
22331
|
+
function formatProposalOutcome(routing, proposal, detail) {
|
|
22332
|
+
if (detail.error) {
|
|
22333
|
+
return `
|
|
22334
|
+
|
|
22335
|
+
---
|
|
22336
|
+
|
|
22337
|
+
**Proposal not recorded.** ${detail.error}
|
|
22338
|
+
|
|
22339
|
+
The build itself is unaffected \u2014 only the proposal failed.`;
|
|
22340
|
+
}
|
|
22341
|
+
switch (routing.outcome) {
|
|
22342
|
+
case "queued-decision":
|
|
22343
|
+
return `
|
|
22344
|
+
|
|
22345
|
+
---
|
|
22346
|
+
|
|
22347
|
+
**Decision proposed \u2014 waiting on you.**
|
|
22348
|
+
|
|
22349
|
+
> ${proposal.title}
|
|
22350
|
+
|
|
22351
|
+
${routing.reason}` + (detail.taskId ? ` It is parked as ${detail.taskId}, blocked on your call, and shows up in \`orient\` as a decision waiting on you. Nothing was written as an Active Decision.` : " Nothing was written as an Active Decision.");
|
|
22352
|
+
case "recorded-convention":
|
|
22353
|
+
return `
|
|
22354
|
+
|
|
22355
|
+
---
|
|
22356
|
+
|
|
22357
|
+
**Recorded as a convention.**
|
|
22358
|
+
|
|
22359
|
+
> ${proposal.title}
|
|
22360
|
+
|
|
22361
|
+
${routing.reason}` + (proposal.module ? ` It rides every future build in the **${proposal.module}** module.` : " It rides every future build on this project.") + (detail.convention?.id ? ` Remove it with \`convention_remove\` (id \`${detail.convention.id}\`) if that is not what you wanted.` : "");
|
|
22362
|
+
case "rejected":
|
|
22363
|
+
return `
|
|
22364
|
+
|
|
22365
|
+
---
|
|
22366
|
+
|
|
22367
|
+
**Proposal not minted, and here is why.**
|
|
22368
|
+
|
|
22369
|
+
> ${proposal.title}
|
|
22370
|
+
|
|
22371
|
+
Failed test (${routing.failedTest}): ${routing.reason}`;
|
|
22372
|
+
}
|
|
22373
|
+
}
|
|
21684
22374
|
|
|
21685
22375
|
// src/tools/doc-registry.ts
|
|
21686
22376
|
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
@@ -22516,7 +23206,7 @@ var buildDescribeTool = {
|
|
|
22516
23206
|
};
|
|
22517
23207
|
var buildExecuteTool = {
|
|
22518
23208
|
name: "build_execute",
|
|
22519
|
-
description: "
|
|
23209
|
+
description: "Build one task with its spec, its branch and its record kept for you. Starting a build hands back that task's BUILD HANDOFF (scope, what is out of scope, acceptance criteria, the decisions that constrain it, and what recent builds in the same area already learned or ruled out), puts the work on the right branch, and marks the task In Progress so a second session can see it is taken. Completing it records what actually happened: effort against estimate, surprises, dead ends, and bugs found outside the scope. That record is what later cycles are sized and planned from, so nothing has to be remembered. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
22520
23210
|
annotations: { title: "Run Build", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22521
23211
|
inputSchema: {
|
|
22522
23212
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -22555,6 +23245,27 @@ var buildExecuteTool = {
|
|
|
22555
23245
|
},
|
|
22556
23246
|
required: ["answer"]
|
|
22557
23247
|
},
|
|
23248
|
+
proposal: {
|
|
23249
|
+
type: "object",
|
|
23250
|
+
description: 'task-3273, OPTIONAL: propose a Decision or a Convention you settled while building this. PAPI never mints one for you \u2014 a proposal that passes all four admission tests is QUEUED for the owner behind a decision gate, and nothing is written as an Active Decision. YOU apply the four tests (they are stated in the planning prompts); the server routes on your answers and makes no judgement about the content. Fails "arguable today" only and it is recorded as a Convention instead, riding every future build. Fails "constrains future work" and it is returned to you with the reason, unrecorded.',
|
|
23251
|
+
properties: {
|
|
23252
|
+
title: { type: "string", description: "One line stating the stance or rule." },
|
|
23253
|
+
body: { type: "string", description: "What was decided or settled, and why." },
|
|
23254
|
+
module: { type: "string", description: "Optional module scope. Only used when this lands as a Convention." },
|
|
23255
|
+
tests: {
|
|
23256
|
+
type: "object",
|
|
23257
|
+
description: "The four admission tests, each answered explicitly. An omitted test is NOT a failed test \u2014 the proposal is refused rather than routed.",
|
|
23258
|
+
properties: {
|
|
23259
|
+
alternativesWereReal: { type: "boolean", description: "(a) Something else could genuinely have been chosen." },
|
|
23260
|
+
constrainsFutureWork: { type: "boolean", description: "(b) It changes what a task nobody has written yet will do." },
|
|
23261
|
+
arguableToday: { type: "boolean", description: "(c) A competent person could argue the other side right now." },
|
|
23262
|
+
reversalCostsMore: { type: "boolean", description: "(d) Reversing it costs more than making it did." }
|
|
23263
|
+
},
|
|
23264
|
+
required: ["alternativesWereReal", "constrainsFutureWork", "arguableToday", "reversalCostsMore"]
|
|
23265
|
+
}
|
|
23266
|
+
},
|
|
23267
|
+
required: ["title", "body", "tests"]
|
|
23268
|
+
},
|
|
22558
23269
|
completed: {
|
|
22559
23270
|
type: "string",
|
|
22560
23271
|
enum: ["yes", "no", "partial"],
|
|
@@ -23049,7 +23760,12 @@ These approaches were tried in this module and failed. If one looks right, read
|
|
|
23049
23760
|
}
|
|
23050
23761
|
} catch {
|
|
23051
23762
|
}
|
|
23052
|
-
|
|
23763
|
+
let conventions = [];
|
|
23764
|
+
try {
|
|
23765
|
+
if (adapter2.listConventions) conventions = await adapter2.listConventions();
|
|
23766
|
+
} catch {
|
|
23767
|
+
}
|
|
23768
|
+
const moduleInstructions = getBuilderInstructions(result.task.module, conventions);
|
|
23053
23769
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
23054
23770
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
23055
23771
|
const modelNote = buildModelRecommendationDirective(
|
|
@@ -23329,7 +24045,27 @@ If any are now fixed, re-run complete with their UUIDs in \`fixed_issues\` \u201
|
|
|
23329
24045
|
fixedNote = "\n\n\u2139\uFE0F This build filed discovered issues but passed no `fixed_issues`. When a future build fixes one, pass its UUID in `fixed_issues` so it counts as FIXED (not just auto-cleared) on the hub ledger.";
|
|
23330
24046
|
}
|
|
23331
24047
|
}
|
|
23332
|
-
|
|
24048
|
+
let proposalNote = "";
|
|
24049
|
+
if (args.proposal !== void 0) {
|
|
24050
|
+
const validated = validateProposal(args.proposal);
|
|
24051
|
+
if ("error" in validated) {
|
|
24052
|
+
proposalNote = `
|
|
24053
|
+
|
|
24054
|
+
---
|
|
24055
|
+
|
|
24056
|
+
**Proposal not recorded.** ${validated.error}
|
|
24057
|
+
|
|
24058
|
+
The build itself is unaffected \u2014 only the proposal failed.`;
|
|
24059
|
+
} else {
|
|
24060
|
+
const routing = routeProposal(validated.proposal);
|
|
24061
|
+
const applied = await applyProposal(adapter2, routing, validated.proposal, {
|
|
24062
|
+
cycleNumber: result.cycleNumber,
|
|
24063
|
+
sourceTaskId: result.task?.id
|
|
24064
|
+
});
|
|
24065
|
+
proposalNote = formatProposalOutcome(routing, validated.proposal, applied);
|
|
24066
|
+
}
|
|
24067
|
+
}
|
|
24068
|
+
return textResponse(formatCompleteResult(result) + fixedNote + docsNote + batchRollupNote + proposalNote);
|
|
23333
24069
|
} catch (err) {
|
|
23334
24070
|
const message = err instanceof Error ? err.message : String(err);
|
|
23335
24071
|
if (isBuildPushError(err)) {
|
|
@@ -24519,6 +25255,27 @@ var adHocTool = {
|
|
|
24519
25255
|
inputSchema: {
|
|
24520
25256
|
type: "object",
|
|
24521
25257
|
properties: {
|
|
25258
|
+
proposal: {
|
|
25259
|
+
type: "object",
|
|
25260
|
+
description: "task-3273, OPTIONAL: propose a Decision or a Convention you settled while doing this work. PAPI never mints one for you \u2014 a proposal passing all four admission tests is QUEUED for the owner behind a decision gate, and nothing is written as an Active Decision. YOU apply the four tests; the server routes on your answers and makes no judgement about the content.",
|
|
25261
|
+
properties: {
|
|
25262
|
+
title: { type: "string", description: "One line stating the stance or rule." },
|
|
25263
|
+
body: { type: "string", description: "What was decided or settled, and why." },
|
|
25264
|
+
module: { type: "string", description: "Optional module scope. Only used when this lands as a Convention." },
|
|
25265
|
+
tests: {
|
|
25266
|
+
type: "object",
|
|
25267
|
+
description: "The four admission tests, each answered explicitly. An omitted test is NOT a failed test.",
|
|
25268
|
+
properties: {
|
|
25269
|
+
alternativesWereReal: { type: "boolean" },
|
|
25270
|
+
constrainsFutureWork: { type: "boolean" },
|
|
25271
|
+
arguableToday: { type: "boolean" },
|
|
25272
|
+
reversalCostsMore: { type: "boolean" }
|
|
25273
|
+
},
|
|
25274
|
+
required: ["alternativesWereReal", "constrainsFutureWork", "arguableToday", "reversalCostsMore"]
|
|
25275
|
+
}
|
|
25276
|
+
},
|
|
25277
|
+
required: ["title", "body", "tests"]
|
|
25278
|
+
},
|
|
24522
25279
|
title: {
|
|
24523
25280
|
type: "string",
|
|
24524
25281
|
description: "What was done \u2014 becomes the task title (required when creating new task, optional when completing existing via task_id)."
|
|
@@ -24578,6 +25335,22 @@ var adHocTool = {
|
|
|
24578
25335
|
required: []
|
|
24579
25336
|
}
|
|
24580
25337
|
};
|
|
25338
|
+
async function recordAdHocProposal(adapter2, args, ctx) {
|
|
25339
|
+
if (args.proposal === void 0) return "";
|
|
25340
|
+
const validated = validateProposal(args.proposal);
|
|
25341
|
+
if ("error" in validated) {
|
|
25342
|
+
return `
|
|
25343
|
+
|
|
25344
|
+
---
|
|
25345
|
+
|
|
25346
|
+
**Proposal not recorded.** ${validated.error}
|
|
25347
|
+
|
|
25348
|
+
The ad-hoc record itself is unaffected \u2014 only the proposal failed.`;
|
|
25349
|
+
}
|
|
25350
|
+
const routing = routeProposal(validated.proposal);
|
|
25351
|
+
const applied = await applyProposal(adapter2, routing, validated.proposal, ctx);
|
|
25352
|
+
return formatProposalOutcome(routing, validated.proposal, applied);
|
|
25353
|
+
}
|
|
24581
25354
|
async function handleAdHoc(adapter2, config2, args) {
|
|
24582
25355
|
const taskId = args.task_id?.trim();
|
|
24583
25356
|
const title = args.title?.trim();
|
|
@@ -24693,12 +25466,12 @@ The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**,
|
|
|
24693
25466
|
\`\`\`
|
|
24694
25467
|
2. Leave the branch **unmerged** \u2014 it is picked up by the next cycle's \`release\`.
|
|
24695
25468
|
|
|
24696
|
-
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25469
|
+
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id })
|
|
24697
25470
|
);
|
|
24698
25471
|
}
|
|
24699
25472
|
return textResponse(
|
|
24700
25473
|
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
24701
|
-
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25474
|
+
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id })
|
|
24702
25475
|
);
|
|
24703
25476
|
}
|
|
24704
25477
|
|
|
@@ -25533,11 +26306,13 @@ async function assembleReviewContext(adapter2, config2, taskId) {
|
|
|
25533
26306
|
${JSON.stringify(task.buildHandoff, null, 2)}` : "### BUILD HANDOFF\n(none recorded)";
|
|
25534
26307
|
const report = task.buildReport ? `### Build Report
|
|
25535
26308
|
${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
25536
|
-
const diff =
|
|
25537
|
-
const
|
|
26309
|
+
const { diff, scope, detail } = getTaskDiff(config2.projectRoot, taskId);
|
|
26310
|
+
const diffHeading = scope === "task-commits" ? `### Diff for ${taskId} (${detail})` : `### \u26A0\uFE0F Diff NOT scoped to ${taskId} (${detail})`;
|
|
26311
|
+
const diffBlock = diff ? `${diffHeading}
|
|
25538
26312
|
\`\`\`diff
|
|
25539
26313
|
${diff}
|
|
25540
|
-
\`\`\`` :
|
|
26314
|
+
\`\`\`` : `### Diff for ${taskId}
|
|
26315
|
+
(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)`;
|
|
25541
26316
|
let projectContext = "";
|
|
25542
26317
|
const ctxPath = join15(config2.projectRoot, ".agents", "papi-context.md");
|
|
25543
26318
|
if (existsSync10(ctxPath)) {
|
|
@@ -27979,6 +28754,33 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
27979
28754
|
}
|
|
27980
28755
|
return lines.join("\n").trimEnd();
|
|
27981
28756
|
}
|
|
28757
|
+
var NOT_SET_UP_MESSAGE = "Setup required \u2014 this project has no PAPI state yet. That is the normal starting point, not an error. Run `setup` to generate your Product Brief and scaffold the workflow, then `plan` to create your first cycle.";
|
|
28758
|
+
function emptyHealthSummary() {
|
|
28759
|
+
return {
|
|
28760
|
+
cycleNumber: 0,
|
|
28761
|
+
// 'degraded' is exactly what happened: the adapter answered some reads and
|
|
28762
|
+
// not others. 'offline' would overstate it and 'connected' would hide it.
|
|
28763
|
+
connectionStatus: "degraded",
|
|
28764
|
+
reviewWarning: "",
|
|
28765
|
+
zoomOutWarning: "",
|
|
28766
|
+
boardSummary: "",
|
|
28767
|
+
staleTasks: "",
|
|
28768
|
+
inReviewSummary: "",
|
|
28769
|
+
carryForward: "",
|
|
28770
|
+
recommendedMode: "",
|
|
28771
|
+
metricsSection: "",
|
|
28772
|
+
derivedMetricsSection: "",
|
|
28773
|
+
costSection: "",
|
|
28774
|
+
decisionUsageSection: "",
|
|
28775
|
+
decisionLifecycleSection: "",
|
|
28776
|
+
decisionScoresSection: "",
|
|
28777
|
+
contextUtilisationSection: "",
|
|
28778
|
+
northStarSection: "",
|
|
28779
|
+
healthScore: null,
|
|
28780
|
+
healthStatus: null,
|
|
28781
|
+
healthReason: null
|
|
28782
|
+
};
|
|
28783
|
+
}
|
|
27982
28784
|
async function getHierarchyPosition(adapter2, projectId) {
|
|
27983
28785
|
try {
|
|
27984
28786
|
const [horizons, stages, phases, allTasks] = await Promise.all([
|
|
@@ -28178,11 +28980,18 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28178
28980
|
} catch {
|
|
28179
28981
|
}
|
|
28180
28982
|
tracker.mark("fetch-build-health-hierarchy");
|
|
28181
|
-
const [
|
|
28983
|
+
const [buildSettled, healthSettled, hierarchySettled] = await Promise.allSettled([
|
|
28182
28984
|
tracked("listBuilds", () => listBuilds(adapter2, config2))(),
|
|
28183
28985
|
tracked("getHealthSummary", () => getHealthSummary(adapter2))(),
|
|
28184
28986
|
tracked("getHierarchyPosition", () => getHierarchyPosition(adapter2, config2.projectId))()
|
|
28185
28987
|
]);
|
|
28988
|
+
if (buildSettled.status === "rejected" && healthSettled.status === "rejected") {
|
|
28989
|
+
throw new Error(NOT_SET_UP_MESSAGE);
|
|
28990
|
+
}
|
|
28991
|
+
if (buildSettled.status === "rejected") throw buildSettled.reason;
|
|
28992
|
+
const buildResult = buildSettled.value;
|
|
28993
|
+
const healthResult = healthSettled.status === "fulfilled" ? healthSettled.value : emptyHealthSummary();
|
|
28994
|
+
const hierarchy = hierarchySettled.status === "fulfilled" ? hierarchySettled.value : void 0;
|
|
28186
28995
|
const currentCycle = buildResult.currentCycle;
|
|
28187
28996
|
const cycleIsComplete = healthResult.latestCycleStatus === "complete";
|
|
28188
28997
|
const allTasks = buildResult.sorted;
|
|
@@ -31087,7 +31896,10 @@ var PAPI_TOOLS = [
|
|
|
31087
31896
|
taskClaimTool,
|
|
31088
31897
|
taskUnclaimTool,
|
|
31089
31898
|
taskMoveTool,
|
|
31090
|
-
inventorySyncTool
|
|
31899
|
+
inventorySyncTool,
|
|
31900
|
+
conventionDeclareTool,
|
|
31901
|
+
conventionListTool,
|
|
31902
|
+
conventionRemoveTool
|
|
31091
31903
|
];
|
|
31092
31904
|
function getToolMetadata() {
|
|
31093
31905
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
@@ -31292,6 +32104,12 @@ function createServer(adapter2, config2) {
|
|
|
31292
32104
|
return handleTaskMove(adapter2, config2, safeArgs);
|
|
31293
32105
|
case "task_unclaim":
|
|
31294
32106
|
return handleTaskUnclaim(adapter2, config2, safeArgs);
|
|
32107
|
+
case "convention_declare":
|
|
32108
|
+
return handleConventionDeclare(adapter2, config2, safeArgs);
|
|
32109
|
+
case "convention_list":
|
|
32110
|
+
return handleConventionList(adapter2, config2, safeArgs);
|
|
32111
|
+
case "convention_remove":
|
|
32112
|
+
return handleConventionRemove(adapter2, config2, safeArgs);
|
|
31295
32113
|
case "inventory_sync":
|
|
31296
32114
|
return handleInventorySync(adapter2, config2, safeArgs, getToolMetadata());
|
|
31297
32115
|
default:
|