pipe-kan 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ bunx pipe-kan --plane --projects APH,PULSE,PKAN,PUI,DEC
57
57
  | `PLANE_API_KEY` | (required in Plane mode) | personal access token (`X-API-Key`) |
58
58
  | `PLANE_HOST` | `https://plane.tail48fe8.ts.net` | Plane origin; API is `{host}/api/v1` |
59
59
 
60
- Same `--projects` style as Jira: comma-separated, trimmed, uppercased. The header **Scope flags** field accepts the same string, then **Refresh**.
60
+ Same `--projects` style as Jira: comma-separated, trimmed, uppercased. The header **Scope flags** field accepts the same string, then **Refresh**. In Plane mode a header **Workspace** pill (beside the project-identifier chip) types a slug, writes `--workspace` into Scope flags, and Refresh-confirms all Epics. Jira omits the pill. Next launch still uses argv `--workspace`, else `PLANE_WORKSPACE`, else `personal`.
61
61
 
62
62
  On Refresh:
63
63
 
package/dist/pipe-kan.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/server.ts
4
- import { readFileSync as readFileSync6, writeSync } from "node:fs";
4
+ import { readFileSync as readFileSync7, writeSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { tmpdir as tmpdir3 } from "node:os";
7
- import { join as join10 } from "node:path";
7
+ import { join as join11 } from "node:path";
8
8
 
9
9
  // src/app.ts
10
10
  import { readFileSync as readFileSync2 } from "node:fs";
@@ -399,8 +399,16 @@ function parseFlags(flags) {
399
399
  statusEq.push(status);
400
400
  continue;
401
401
  }
402
- if (token === "--projects") {
403
- [projectsValue, i] = takeValue(list, i, "--projects");
402
+ if (token === "--projects" || token.startsWith("--projects=")) {
403
+ const parts = [];
404
+ if (token.startsWith("--projects=")) {
405
+ parts.push(token.slice("--projects=".length));
406
+ }
407
+ while (i + 1 < list.length && !list[i + 1].startsWith("-")) {
408
+ i++;
409
+ parts.push(list[i]);
410
+ }
411
+ projectsValue = parts.join(",");
404
412
  continue;
405
413
  }
406
414
  if (token === "-q" || token === "--jql") {
@@ -1504,6 +1512,12 @@ function createApp(opts) {
1504
1512
  error: err instanceof Error ? err.message : "jira issue view failed"
1505
1513
  };
1506
1514
  }
1515
+ },
1516
+ async listWorkspaces() {
1517
+ return cli.listWorkspaces?.() ?? [];
1518
+ },
1519
+ async listProjectIdentifiers(workspace) {
1520
+ return cli.listProjectIdentifiers?.(workspace) ?? [];
1507
1521
  }
1508
1522
  };
1509
1523
  return app;
@@ -1877,6 +1891,19 @@ function createPlaneCli(opts) {
1877
1891
  return;
1878
1892
  return { id, identifier, name: asString(row?.name) ?? identifier };
1879
1893
  }
1894
+ function parseWorkspace(raw) {
1895
+ const row = asRecord(raw);
1896
+ const slug = asString(row?.slug);
1897
+ if (!slug)
1898
+ return;
1899
+ return { id: asString(row?.id) ?? slug, name: asString(row?.name) ?? slug, slug };
1900
+ }
1901
+ function parseWorkspaceList(raw) {
1902
+ const rows = Array.isArray(raw) ? raw : asRecord(raw)?.results;
1903
+ if (!Array.isArray(rows))
1904
+ return;
1905
+ return rows.map(parseWorkspace).filter((row) => !!row);
1906
+ }
1880
1907
  function parseState(raw) {
1881
1908
  const row = asRecord(raw);
1882
1909
  const id = asString(row?.id);
@@ -2092,6 +2119,25 @@ function createPlaneCli(opts) {
2092
2119
  const loaded = await loadCatalog(flags || defaultFlags);
2093
2120
  return loaded.columns;
2094
2121
  },
2122
+ async listWorkspaces() {
2123
+ for (const path of ["/users/me/workspaces/", "/workspaces/"]) {
2124
+ try {
2125
+ return parseWorkspaceList(await requestJson(path)) ?? [];
2126
+ } catch (err) {
2127
+ const status = err.status;
2128
+ if (status === 404 && path === "/users/me/workspaces/")
2129
+ continue;
2130
+ return [];
2131
+ }
2132
+ }
2133
+ return [];
2134
+ },
2135
+ async listProjectIdentifiers(workspace) {
2136
+ const slug = workspace.trim();
2137
+ if (!slug)
2138
+ return [];
2139
+ return (await paginate(`/workspaces/${slug}/projects/`)).map(parseProject).filter((row) => !!row).map((row) => row.identifier);
2140
+ },
2095
2141
  async move(key, status) {
2096
2142
  try {
2097
2143
  const module = (await loadCatalog(lastFlags)).modules.get(key);
@@ -2234,6 +2280,123 @@ async function refreshFromJira(app, kind, opts = {}) {
2234
2280
  await app.refresh();
2235
2281
  }
2236
2282
 
2283
+ // src/factory-ledger.ts
2284
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "node:fs";
2285
+ import { homedir as homedir3 } from "node:os";
2286
+ import { join as join4 } from "node:path";
2287
+ var DEFAULT_LEDGER_PATH = "~/code/software-factory/asf/runtime/factory-coordinator/jobs.jsonl";
2288
+ function resolveLedgerPath(env = process.env) {
2289
+ const raw = env.FACTORY_LEDGER_PATH ?? DEFAULT_LEDGER_PATH;
2290
+ return raw.startsWith("~/") ? join4(homedir3(), raw.slice(2)) : raw;
2291
+ }
2292
+ function parseJobsJsonl(text) {
2293
+ const events = [];
2294
+ for (const line of text.split(`
2295
+ `)) {
2296
+ const trimmed = line.trim();
2297
+ if (!trimmed)
2298
+ continue;
2299
+ try {
2300
+ events.push(JSON.parse(trimmed));
2301
+ } catch {}
2302
+ }
2303
+ return events;
2304
+ }
2305
+ function lc(...parts) {
2306
+ return parts.filter(Boolean).join(" ").toLowerCase();
2307
+ }
2308
+ function deriveStageTag(events) {
2309
+ if (!events.length)
2310
+ return null;
2311
+ const last = events[events.length - 1];
2312
+ if (last.status === "done" || last.status === "cancelled")
2313
+ return null;
2314
+ const text = lc(last.detail, last.summary, last.target_agent, last.from_bot);
2315
+ const fromBot = lc(last.from_bot);
2316
+ const targetAgent = lc(last.target_agent);
2317
+ const detail = lc(last.detail);
2318
+ if (last.status === "blocked" || /needs.{0,4}judgment|green.{0,4}light|await.{0,4}ship|awaiting.{0,4}ship|needs.{0,4}input|waiting.{0,4}operator|checks.{0,4}pass|verify.{0,4}pass|human.{0,4}gate/.test(text)) {
2319
+ return "human-wait";
2320
+ }
2321
+ if (last.status === "queued" || last.status === "routed" || /grokbot.{0,4}coordinator|intake.{0,4}switchboard/.test(fromBot) || /routing|switchboard/.test(text)) {
2322
+ return "coordinator";
2323
+ }
2324
+ if (/planner|plan_|stage_1a/.test(targetAgent) || /planner|to.{0,2}spec|to.{0,2}tickets|grill.{0,4}with.{0,4}docs|stage_1/.test(text)) {
2325
+ return "planning";
2326
+ }
2327
+ if (/desk=/.test(detail) || /preflight|research.{0,4}gate|research.{0,4}dossier/.test(text)) {
2328
+ return "desk";
2329
+ }
2330
+ if (/maker|implement|referee|omp|stage_2/.test(targetAgent) || /\bmaker\b|implement|referee|stage_2|pr.{0,4}open|pull.{0,4}request/.test(text)) {
2331
+ return "maker";
2332
+ }
2333
+ return "coordinator";
2334
+ }
2335
+ function extractPrUrl(detail) {
2336
+ if (!detail)
2337
+ return;
2338
+ const m = /https:\/\/github\.com\/[^\s"']+\/pull\/\d+/.exec(detail);
2339
+ return m?.[0];
2340
+ }
2341
+ function groupByLatest(events) {
2342
+ const map = new Map;
2343
+ for (const ev of events) {
2344
+ if (!ev.id)
2345
+ continue;
2346
+ const list = map.get(ev.id);
2347
+ if (list) {
2348
+ list.push(ev);
2349
+ } else {
2350
+ map.set(ev.id, [ev]);
2351
+ }
2352
+ }
2353
+ return map;
2354
+ }
2355
+ function jobFromEvents(id, events) {
2356
+ const last = events[events.length - 1];
2357
+ const pullRequestUrl = events.map((e) => e.pull_request_url ?? extractPrUrl(e.detail)).find(Boolean);
2358
+ return {
2359
+ id,
2360
+ summary: last.summary ?? "",
2361
+ status: last.status ?? "",
2362
+ stageTag: deriveStageTag(events),
2363
+ latestTs: last.ts,
2364
+ ...pullRequestUrl ? { pullRequestUrl } : {},
2365
+ ...last.plane_project ? { planeProject: last.plane_project } : {},
2366
+ ...last.detail ? { detail: last.detail } : {},
2367
+ ...last.session ? { session: last.session } : {},
2368
+ ...last.from_bot ? { fromBot: last.from_bot } : {}
2369
+ };
2370
+ }
2371
+ function classifyJobs(events) {
2372
+ const grouped = groupByLatest(events);
2373
+ const jobs = [];
2374
+ for (const [id, evs] of grouped) {
2375
+ jobs.push(jobFromEvents(id, evs));
2376
+ }
2377
+ jobs.sort((a, b) => b.latestTs.localeCompare(a.latestTs));
2378
+ return jobs;
2379
+ }
2380
+ var _cache = null;
2381
+ function readLedger(env = process.env) {
2382
+ const path = resolveLedgerPath(env);
2383
+ if (!existsSync2(path)) {
2384
+ return { jobs: [], error: `Ledger not found: ${path}` };
2385
+ }
2386
+ try {
2387
+ const mtime = statSync(path).mtimeMs;
2388
+ if (_cache && _cache.path === path && _cache.mtime === mtime) {
2389
+ return { jobs: _cache.jobs };
2390
+ }
2391
+ const text = readFileSync3(path, "utf8");
2392
+ const jobs = classifyJobs(parseJobsJsonl(text));
2393
+ _cache = { path, mtime, jobs };
2394
+ return { jobs };
2395
+ } catch (err) {
2396
+ return { jobs: [], error: String(err) };
2397
+ }
2398
+ }
2399
+
2237
2400
  // src/app-api.ts
2238
2401
  function json(res, status, body) {
2239
2402
  res.statusCode = status;
@@ -2259,11 +2422,46 @@ function reply(req, res, work) {
2259
2422
  json(res, 500, { error: message });
2260
2423
  });
2261
2424
  }
2262
- function handleAppApi(req, res, app) {
2425
+ async function boardEnvelope(body, kind, flags, env, app) {
2426
+ if (kind !== "plane")
2427
+ return { ...body, kind };
2428
+ const envelope = { ...body, kind, workspace: planeWorkspace(flags, env) };
2429
+ try {
2430
+ const workspaces = await app.listWorkspaces?.();
2431
+ if (workspaces && workspaces.length)
2432
+ return { ...envelope, workspaces };
2433
+ } catch {}
2434
+ return envelope;
2435
+ }
2436
+ function handleAppApi(req, res, app, opts = {}) {
2437
+ const kind = opts.kind ?? "store";
2438
+ const env = opts.env ?? process.env;
2263
2439
  const url = pathOf(req);
2264
2440
  const method = (req.method ?? "GET").toUpperCase();
2265
2441
  if (url.pathname === "/api/board" && method === "GET") {
2266
- json(res, 200, { ...app.board(), flags: app.flags });
2442
+ reply(req, res, async () => {
2443
+ json(res, 200, await boardEnvelope({ ...app.board(), flags: app.flags }, kind, app.flags, env, app));
2444
+ });
2445
+ return true;
2446
+ }
2447
+ if (url.pathname === "/api/projects" && method === "GET") {
2448
+ reply(req, res, async () => {
2449
+ if (kind !== "plane") {
2450
+ json(res, 404, { error: "not plane" });
2451
+ return;
2452
+ }
2453
+ const workspace = (url.searchParams.get("workspace") ?? "").trim();
2454
+ if (!workspace) {
2455
+ json(res, 400, { error: "workspace required" });
2456
+ return;
2457
+ }
2458
+ try {
2459
+ json(res, 200, { projects: await app.listProjectIdentifiers?.(workspace) ?? [] });
2460
+ } catch (err) {
2461
+ const message = err instanceof Error ? err.message : "catalog failed";
2462
+ json(res, 409, { error: message });
2463
+ }
2464
+ });
2267
2465
  return true;
2268
2466
  }
2269
2467
  if (url.pathname === "/api/refresh" && method === "POST") {
@@ -2275,10 +2473,10 @@ function handleAppApi(req, res, app) {
2275
2473
  json(res, 400, { error: "selected Refresh requires epicKeys" });
2276
2474
  return;
2277
2475
  }
2278
- json(res, 200, await app.refresh(undefined, { scope: "selected", epicKeys }));
2476
+ json(res, 200, await boardEnvelope(await app.refresh(undefined, { scope: "selected", epicKeys }), kind, app.flags, env, app));
2279
2477
  return;
2280
2478
  }
2281
- json(res, 200, await app.refresh(body.flags));
2479
+ json(res, 200, await boardEnvelope(await app.refresh(body.flags), kind, app.flags, env, app));
2282
2480
  });
2283
2481
  return true;
2284
2482
  }
@@ -2343,6 +2541,26 @@ function handleAppApi(req, res, app) {
2343
2541
  });
2344
2542
  return true;
2345
2543
  }
2544
+ if (url.pathname === "/api/factory/jobs" && method === "GET") {
2545
+ reply(req, res, async () => {
2546
+ const result = readLedger();
2547
+ json(res, result.error && result.jobs.length === 0 ? 503 : 200, result);
2548
+ });
2549
+ return true;
2550
+ }
2551
+ if (/^\/api\/factory\/jobs\/[^/]+$/.test(url.pathname) && method === "GET") {
2552
+ reply(req, res, async () => {
2553
+ const id = url.pathname.slice("/api/factory/jobs/".length);
2554
+ const result = readLedger();
2555
+ const job = result.jobs.find((j) => j.id === id);
2556
+ if (!job) {
2557
+ json(res, 404, { error: `Job not found: ${id}`, ledgerError: result.error });
2558
+ return;
2559
+ }
2560
+ json(res, 200, { job });
2561
+ });
2562
+ return true;
2563
+ }
2346
2564
  return false;
2347
2565
  }
2348
2566
 
@@ -2432,10 +2650,10 @@ function formatCard(card) {
2432
2650
  }
2433
2651
 
2434
2652
  // src/server/agent/config.ts
2435
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2436
- import { homedir as homedir3 } from "node:os";
2437
- import { delimiter as delimiter2, dirname as dirname2, join as join4 } from "node:path";
2438
- var DEFAULT_CONFIG_PATH = join4(homedir3(), ".config", "pipe-kan", "agent.json");
2653
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2654
+ import { homedir as homedir4 } from "node:os";
2655
+ import { delimiter as delimiter2, dirname as dirname2, join as join5 } from "node:path";
2656
+ var DEFAULT_CONFIG_PATH = join5(homedir4(), ".config", "pipe-kan", "agent.json");
2439
2657
  var FALLBACK_MODELS = {
2440
2658
  cursor: [
2441
2659
  { id: "composer-2", name: "Composer 2" },
@@ -2477,21 +2695,21 @@ function commandOnPath(command, env = process.env) {
2477
2695
  if (!command)
2478
2696
  return false;
2479
2697
  if (command.includes("/") || command.includes("\\"))
2480
- return existsSync2(command);
2698
+ return existsSync3(command);
2481
2699
  const pathVar = env.PATH ?? "";
2482
2700
  for (const dir of pathVar.split(delimiter2)) {
2483
2701
  if (!dir)
2484
2702
  continue;
2485
- if (existsSync2(join4(dir, command)))
2703
+ if (existsSync3(join5(dir, command)))
2486
2704
  return true;
2487
2705
  }
2488
2706
  return false;
2489
2707
  }
2490
2708
  function loadAgentConfig(path = agentConfigPath()) {
2491
- if (!existsSync2(path))
2709
+ if (!existsSync3(path))
2492
2710
  return structuredClone(DEFAULT_CONFIG);
2493
2711
  try {
2494
- const raw = JSON.parse(readFileSync3(path, "utf8"));
2712
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
2495
2713
  return mergeConfig(raw);
2496
2714
  } catch {
2497
2715
  return structuredClone(DEFAULT_CONFIG);
@@ -2527,7 +2745,7 @@ function mergeConfig(raw) {
2527
2745
  }
2528
2746
  function ensureAgentConfigDir(path = agentConfigPath()) {
2529
2747
  const dir = dirname2(path);
2530
- if (!existsSync2(dir))
2748
+ if (!existsSync3(dir))
2531
2749
  mkdirSync2(dir, { recursive: true });
2532
2750
  }
2533
2751
 
@@ -2535,7 +2753,7 @@ function ensureAgentConfigDir(path = agentConfigPath()) {
2535
2753
  import { spawn as spawn2 } from "node:child_process";
2536
2754
  import { mkdtempSync, rmSync } from "node:fs";
2537
2755
  import { tmpdir as tmpdir2 } from "node:os";
2538
- import { join as join5 } from "node:path";
2756
+ import { join as join6 } from "node:path";
2539
2757
  import { Readable } from "node:stream";
2540
2758
 
2541
2759
  // node_modules/@agentclientprotocol/sdk/dist/schema/index.js
@@ -12261,7 +12479,7 @@ class AcpSession {
12261
12479
  executeTool = null;
12262
12480
  stderr = "";
12263
12481
  selectedModelId = null;
12264
- workspace = mkdtempSync(join5(tmpdir2(), "pipe-kan-agent-"));
12482
+ workspace = mkdtempSync(join6(tmpdir2(), "pipe-kan-agent-"));
12265
12483
  constructor(config) {
12266
12484
  this.config = config;
12267
12485
  this.id = crypto.randomUUID();
@@ -12573,17 +12791,17 @@ function permissionOptionForDecision(options, decision) {
12573
12791
  }
12574
12792
 
12575
12793
  // src/server/agent/skills.ts
12576
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "node:fs";
12577
- import { homedir as homedir4 } from "node:os";
12578
- import { join as join6 } from "node:path";
12794
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
12795
+ import { homedir as homedir5 } from "node:os";
12796
+ import { join as join7 } from "node:path";
12579
12797
  import { fileURLToPath } from "node:url";
12580
12798
  function createSkillRegistry(bundledDir) {
12581
- const dir = bundledDir ?? join6(fileURLToPath(new URL(".", import.meta.url)), "..", "..", "..", ".agents", "skills");
12582
- const userDir = join6(homedir4(), ".pi", "agent", "skills");
12799
+ const dir = bundledDir ?? join7(fileURLToPath(new URL(".", import.meta.url)), "..", "..", "..", ".agents", "skills");
12800
+ const userDir = join7(homedir5(), ".pi", "agent", "skills");
12583
12801
  return {
12584
12802
  list() {
12585
12803
  const bundled = listSkills(dir);
12586
- const user = existsSync3(userDir) ? listSkills(userDir) : [];
12804
+ const user = existsSync4(userDir) ? listSkills(userDir) : [];
12587
12805
  const map = new Map;
12588
12806
  for (const skill of bundled)
12589
12807
  map.set(skill.id, skill);
@@ -12593,10 +12811,10 @@ function createSkillRegistry(bundledDir) {
12593
12811
  },
12594
12812
  load(id) {
12595
12813
  const userPath = skillPath(userDir, id);
12596
- if (existsSync3(userPath))
12814
+ if (existsSync4(userPath))
12597
12815
  return readSkill(userPath, id);
12598
12816
  const bundledPath = skillPath(dir, id);
12599
- if (existsSync3(bundledPath))
12817
+ if (existsSync4(bundledPath))
12600
12818
  return readSkill(bundledPath, id);
12601
12819
  return;
12602
12820
  }
@@ -12613,16 +12831,16 @@ function skillContextBlock(skill) {
12613
12831
  };
12614
12832
  }
12615
12833
  function listSkills(dir) {
12616
- if (!existsSync3(dir))
12834
+ if (!existsSync4(dir))
12617
12835
  return [];
12618
12836
  return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readSkill(skillPath(dir, entry.name), entry.name)).filter((skill) => skill !== undefined);
12619
12837
  }
12620
12838
  function skillPath(dir, id) {
12621
- return join6(dir, id, "SKILL.md");
12839
+ return join7(dir, id, "SKILL.md");
12622
12840
  }
12623
12841
  function readSkill(path, id) {
12624
12842
  try {
12625
- const text = readFileSync4(path, "utf8");
12843
+ const text = readFileSync5(path, "utf8");
12626
12844
  const front = parseFrontMatter(text);
12627
12845
  return {
12628
12846
  id,
@@ -12649,8 +12867,8 @@ function parseFrontMatter(text) {
12649
12867
  }
12650
12868
 
12651
12869
  // src/server/agent/tools.ts
12652
- import { readFileSync as readFileSync5 } from "node:fs";
12653
- import { join as join7 } from "node:path";
12870
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
12871
+ import { join as join8 } from "node:path";
12654
12872
  var TOOLS = [
12655
12873
  {
12656
12874
  name: "board_state",
@@ -12726,6 +12944,35 @@ var TOOLS = [
12726
12944
  labels: { type: "string", description: "Comma-separated replacement labels" }
12727
12945
  },
12728
12946
  mutates: true
12947
+ },
12948
+ {
12949
+ name: "factory_job_status",
12950
+ description: "Read the latest status and factory stage tag for one or more software-factory jobs from the local ledger. Pass a job id (gbj-YYYYMMDD-NNN) for a single job, or omit id to get the 20 most-recent active jobs.",
12951
+ parameters: {
12952
+ id: { type: "string", description: "Optional job id, e.g. gbj-20260926-011" }
12953
+ },
12954
+ mutates: false
12955
+ },
12956
+ {
12957
+ name: "factory_start_job",
12958
+ description: "Request the Grokbot Coordinator to start a new factory job. Writes a start-request to docs/factory/outbox/ for the coordinator to pick up. Requires user approval. Does NOT append to jobs.jsonl directly.",
12959
+ parameters: {
12960
+ summary: { type: "string", description: "One-line job summary" },
12961
+ repo: { type: "string", description: "Target repo path, e.g. ~/code/pipe-kan" },
12962
+ session: { type: "string", description: "Herdr session name, e.g. jira-kan" },
12963
+ notes: { type: "string", description: "Optional extra context for the coordinator" }
12964
+ },
12965
+ mutates: true
12966
+ },
12967
+ {
12968
+ name: "factory_move_job",
12969
+ description: "Request the Grokbot Coordinator to move a factory job to a new status or stage. Writes a move-request to docs/factory/outbox/ for the coordinator to pick up. Requires user approval. Does NOT append to jobs.jsonl directly.",
12970
+ parameters: {
12971
+ id: { type: "string", description: "Job id to move, e.g. gbj-20260926-011" },
12972
+ status: { type: "string", description: "Target status: queued|routed|in_progress|blocked|done|cancelled" },
12973
+ detail: { type: "string", description: "Optional detail message for the ledger event" }
12974
+ },
12975
+ mutates: true
12729
12976
  }
12730
12977
  ];
12731
12978
  var skills;
@@ -12770,7 +13017,7 @@ var EXECUTORS = {
12770
13017
  return { ok: false, error: "Path traversal not allowed" };
12771
13018
  const repoRoot = process.cwd();
12772
13019
  try {
12773
- const text = readFileSync5(join7(repoRoot, relPath), "utf8");
13020
+ const text = readFileSync6(join8(repoRoot, relPath), "utf8");
12774
13021
  return { ok: true, value: { path: relPath, text } };
12775
13022
  } catch (err) {
12776
13023
  return { ok: false, error: String(err) };
@@ -12841,6 +13088,93 @@ var EXECUTORS = {
12841
13088
  if (result.error)
12842
13089
  return { ok: false, error: result.error };
12843
13090
  return { ok: true, value: { key, __ui_action: "refresh_board" } };
13091
+ },
13092
+ factory_job_status(args) {
13093
+ const id = typeof args.id === "string" ? args.id.trim() : "";
13094
+ const ledger = readLedger();
13095
+ if (id) {
13096
+ const job = ledger.jobs.find((j) => j.id === id);
13097
+ if (!job) {
13098
+ return { ok: false, error: `Job not found: ${id}${ledger.error ? ` (${ledger.error})` : ""}` };
13099
+ }
13100
+ return { ok: true, value: { job, ledgerError: ledger.error } };
13101
+ }
13102
+ const active = ledger.jobs.filter((j) => j.stageTag !== null).slice(0, 20);
13103
+ return {
13104
+ ok: true,
13105
+ value: { jobs: active, total: ledger.jobs.length, ledgerError: ledger.error }
13106
+ };
13107
+ },
13108
+ factory_start_job(args) {
13109
+ const summary = String(args.summary ?? "").trim();
13110
+ if (!summary)
13111
+ return { ok: false, error: "Missing summary" };
13112
+ const repo = String(args.repo ?? "").trim();
13113
+ const session = String(args.session ?? "").trim();
13114
+ const notes = typeof args.notes === "string" ? args.notes.trim() : undefined;
13115
+ const ts = new Date().toISOString();
13116
+ const requestId = crypto.randomUUID();
13117
+ const outboxDir = join8(process.cwd(), "docs/factory/outbox");
13118
+ mkdirSync3(outboxDir, { recursive: true });
13119
+ const filename = `${ts.replace(/[:.]/g, "-")}-${requestId}-start.json`;
13120
+ const payload = {
13121
+ action: "start_job",
13122
+ requested_by: "pipe-kan-acp",
13123
+ ts,
13124
+ requestId,
13125
+ summary,
13126
+ ...repo ? { repo } : {},
13127
+ ...session ? { session } : {},
13128
+ ...notes ? { notes } : {}
13129
+ };
13130
+ writeFileSync3(join8(outboxDir, filename), JSON.stringify(payload, null, 2) + `
13131
+ `, "utf8");
13132
+ return {
13133
+ ok: true,
13134
+ value: {
13135
+ message: `Start request written to docs/factory/outbox/${filename}. The Grokbot Coordinator will pick this up and append the real ledger event.`,
13136
+ outboxFile: filename
13137
+ }
13138
+ };
13139
+ },
13140
+ factory_move_job(args) {
13141
+ const id = String(args.id ?? "").trim();
13142
+ const status = String(args.status ?? "").trim();
13143
+ if (!id)
13144
+ return { ok: false, error: "Missing job id" };
13145
+ if (!status)
13146
+ return { ok: false, error: "Missing target status" };
13147
+ const validStatuses = ["queued", "routed", "in_progress", "blocked", "done", "cancelled"];
13148
+ if (!validStatuses.includes(status)) {
13149
+ return {
13150
+ ok: false,
13151
+ error: `Invalid status '${status}'. Must be one of: ${validStatuses.join(", ")}`
13152
+ };
13153
+ }
13154
+ const detail = typeof args.detail === "string" ? args.detail.trim() : undefined;
13155
+ const ts = new Date().toISOString();
13156
+ const requestId = crypto.randomUUID();
13157
+ const outboxDir = join8(process.cwd(), "docs/factory/outbox");
13158
+ mkdirSync3(outboxDir, { recursive: true });
13159
+ const filename = `${ts.replace(/[:.]/g, "-")}-${requestId}-move.json`;
13160
+ const payload = {
13161
+ action: "move_job",
13162
+ requested_by: "pipe-kan-acp",
13163
+ ts,
13164
+ requestId,
13165
+ id,
13166
+ status,
13167
+ ...detail ? { detail } : {}
13168
+ };
13169
+ writeFileSync3(join8(outboxDir, filename), JSON.stringify(payload, null, 2) + `
13170
+ `, "utf8");
13171
+ return {
13172
+ ok: true,
13173
+ value: {
13174
+ message: `Move request for ${id} → ${status} written to docs/factory/outbox/${filename}. The Grokbot Coordinator will pick this up and append the real ledger event.`,
13175
+ outboxFile: filename
13176
+ }
13177
+ };
12844
13178
  }
12845
13179
  };
12846
13180
  var SYSTEM_TEXT = `Your working context is the attached Jira issues from the pipe. Answer from that payload. Do not search GitHub, git history, or local source files unless the user explicitly asks about the pipe-kan app itself.
@@ -13307,20 +13641,21 @@ function handleFakeJira(req, res, store) {
13307
13641
 
13308
13642
  // src/http.ts
13309
13643
  function handleRequest(req, res, ctx) {
13310
- if (handleAppApi(req, res, ctx.app) || handleAgentApi(req, res, ctx.app))
13644
+ if (handleAppApi(req, res, ctx.app, { kind: ctx.kind }) || handleAgentApi(req, res, ctx.app)) {
13311
13645
  return true;
13646
+ }
13312
13647
  if (ctx.kind === "plane")
13313
13648
  return false;
13314
13649
  return handleFakeJira(req, res, ctx.store);
13315
13650
  }
13316
13651
 
13317
13652
  // src/jira-config.ts
13318
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
13319
- import { join as join8 } from "node:path";
13653
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
13654
+ import { join as join9 } from "node:path";
13320
13655
  function writeJiraConfig(dir, server) {
13321
- mkdirSync3(dir, { recursive: true });
13322
- const path = join8(dir, "jira.config.yml");
13323
- writeFileSync3(path, [
13656
+ mkdirSync4(dir, { recursive: true });
13657
+ const path = join9(dir, "jira.config.yml");
13658
+ writeFileSync4(path, [
13324
13659
  "installation: Cloud",
13325
13660
  `server: ${server}`,
13326
13661
  `login: ${ME.emailAddress}`,
@@ -13374,8 +13709,8 @@ function stdinStat() {
13374
13709
  }
13375
13710
 
13376
13711
  // src/ui.ts
13377
- import { createReadStream, existsSync as existsSync4, statSync } from "node:fs";
13378
- import { dirname as dirname3, extname, join as join9, resolve as resolve2, sep } from "node:path";
13712
+ import { createReadStream, existsSync as existsSync5, statSync as statSync2 } from "node:fs";
13713
+ import { dirname as dirname3, extname, join as join10, resolve as resolve2, sep } from "node:path";
13379
13714
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13380
13715
  var types = {
13381
13716
  ".css": "text/css; charset=utf-8",
@@ -13392,7 +13727,7 @@ function packageRoot(from = import.meta.url) {
13392
13727
  return resolve2(dirname3(fileURLToPath2(from)), "..");
13393
13728
  }
13394
13729
  function uiDir(root) {
13395
- return join9(root, "dist", "ui");
13730
+ return join10(root, "dist", "ui");
13396
13731
  }
13397
13732
  function inside(root, file) {
13398
13733
  const base = resolve2(root);
@@ -13401,12 +13736,12 @@ function inside(root, file) {
13401
13736
  }
13402
13737
  function sendUi(root, req, res) {
13403
13738
  const ui = uiDir(root);
13404
- const index = join9(ui, "index.html");
13405
- if (!existsSync4(index))
13739
+ const index = join10(ui, "index.html");
13740
+ if (!existsSync5(index))
13406
13741
  return false;
13407
13742
  const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
13408
13743
  const wanted = resolve2(ui, `.${decodeURIComponent(path)}`);
13409
- const file = inside(ui, wanted) && existsSync4(wanted) && statSync(wanted).isFile() ? wanted : index;
13744
+ const file = inside(ui, wanted) && existsSync5(wanted) && statSync2(wanted).isFile() ? wanted : index;
13410
13745
  res.setHeader("content-type", types[extname(file)] ?? "application/octet-stream");
13411
13746
  createReadStream(file).pipe(res);
13412
13747
  return true;
@@ -13422,7 +13757,7 @@ function announce(line) {
13422
13757
  }
13423
13758
  async function runServer(opts) {
13424
13759
  const piped = await readPipe();
13425
- const raw = piped ?? JSON.parse(readFileSync6(join10(opts.root, "fixtures/issues.json"), "utf8"));
13760
+ const raw = piped ?? JSON.parse(readFileSync7(join11(opts.root, "fixtures/issues.json"), "utf8"));
13426
13761
  const flags = argvToFlags(process.argv);
13427
13762
  const { app, store, kind } = await createBoardApp({
13428
13763
  raw,
@@ -13459,7 +13794,7 @@ async function runServer(opts) {
13459
13794
  announce(`cli plane ${planeHost()} ${planeWorkspace(flags)}`);
13460
13795
  announce(`Plane host default ${DEFAULT_PLANE_HOST}`);
13461
13796
  } else {
13462
- const fakeConfig = writeJiraConfig(join10(tmpdir3(), "pipe-kan"), origin);
13797
+ const fakeConfig = writeJiraConfig(join11(tmpdir3(), "pipe-kan"), origin);
13463
13798
  announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
13464
13799
  announce(`Fake Jira ${origin}/rest/api/2/search`);
13465
13800
  announce(`Fake Jira config ${fakeConfig}`);