@brainervirus/workit-core 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -7,7 +7,9 @@ description: Use after creating a PR (auto-starts, drive default), when a PR nee
7
7
 
8
8
  Babysit starts automatically on PR creation unless declined (`babysit:false`).
9
9
  One babysitter per PR; never mutate PR topology (no rebase strategy changes,
10
- no force-push).
10
+ no force-push). When a PR URL is observed from a route Workit did not enforce
11
+ (for example a raw `gh pr create` the host allowed), load this skill and drive
12
+ that PR anyway; do not claim the Workit route was enforced.
11
13
 
12
14
  ## Before method work
13
15
 
@@ -22,9 +22,21 @@ selects requirements; do not wait for a rule that can only exist after assess.
22
22
  needed, owner, and next action. Keep the plan against the existing system.
23
23
  3. If policy separately requires a durable specification, record that behavior
24
24
  agreement; otherwise do not invent a spec. A plan without a spec is valid.
25
- 4. Update the shared task progress at meaningful boundaries. Reassess when facts,
25
+ 4. Present before asking: show durable artifacts as a complete digest plus the
26
+ exact path and inline plans as their content. Never ask the user to approve
27
+ something they have not seen, and keep the question to one short scoped
28
+ sentence.
29
+ 5. Execute an approved plan continuously: one atomic commit per task after its
30
+ checks pass, no "continue?" prompts. Stop only for a new product decision, a
31
+ failed safety or verification gate, a conflicting concurrent edit, or missing
32
+ authority.
33
+ 6. Record the plan's commit list once through the plan-scoped action approval
34
+ (`git.commit` with `plan_steps` and `plan_branch`) so each listed commit runs
35
+ without a new question; an unlisted message, a branch change, or any change to
36
+ the plan needs a fresh exact approval.
37
+ 7. Update the shared task progress at meaningful boundaries. Reassess when facts,
26
38
  dependencies, or scope change; preserve unresolved blockers and decisions.
27
- 5. On steering (new instructions mid-task): apply `workit-steer` — park state
39
+ 8. On steering (new instructions mid-task): apply `workit-steer` — park state
28
40
  verbatim, classify same-task / new-task / quick-question, handle, re-anchor.
29
41
 
30
42
  Use shared task/progress and evidence operations. Do not create a universal
@@ -432,7 +432,15 @@ export const verifyDecisionContentAtRoot = (
432
432
  return failure("invalid_input", "document references require a byte digest");
433
433
  const target = path.resolve(root, reference.path);
434
434
  if (target !== root && !target.startsWith(`${root}${path.sep}`))
435
- return failure("invalid_input", "document reference escapes checkout");
435
+ return failure(
436
+ "invalid_input",
437
+ "document reference escapes checkout; cite it as an external file:// reference instead",
438
+ {
439
+ fields: [
440
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` },
441
+ ],
442
+ },
443
+ );
436
444
  try {
437
445
  const relative = path.relative(root, target);
438
446
  let current = root;
@@ -442,7 +450,18 @@ export const verifyDecisionContentAtRoot = (
442
450
  const stat = fs.lstatSync(current);
443
451
  const real = fs.realpathSync(current);
444
452
  if (real !== root && !real.startsWith(`${root}${path.sep}`))
445
- return failure("invalid_input", "document reference escapes checkout");
453
+ return failure(
454
+ "invalid_input",
455
+ "document reference escapes checkout; cite it as an external file:// reference instead",
456
+ {
457
+ fields: [
458
+ {
459
+ path: "contentRefs.path",
460
+ reason: `${reference.path} resolves outside the checkout`,
461
+ },
462
+ ],
463
+ },
464
+ );
446
465
  if (stat.isSymbolicLink())
447
466
  return failure("invalid_input", "document reference uses a symlink");
448
467
  if (current === target && !stat.isFile())
@@ -268,6 +268,18 @@ export const ensureBaseBranch = (cwd: string, base: string): { ok: boolean; erro
268
268
  return fastForwardBase(cwd, base);
269
269
  };
270
270
 
271
+ export type BranchSetupResult =
272
+ | {
273
+ action: "setup";
274
+ ok: true;
275
+ branch: string;
276
+ previous_branch: string;
277
+ stash_ref: string | null;
278
+ manifest: string;
279
+ }
280
+ | { action: "reapply_stash"; ok: true }
281
+ | { error: string; phase: "preflight" | "post_mutation" };
282
+
271
283
  // Port of scripts/branch/setup-branch.sh
272
284
  export const branchSetup = ({
273
285
  action,
@@ -283,12 +295,13 @@ export const branchSetup = ({
283
295
  stash?: string;
284
296
  workspace_root: string;
285
297
  log?: (message: string) => void;
286
- }) => {
298
+ }): BranchSetupResult => {
287
299
  const cwd = path.resolve(workspace_root);
288
300
  const exec = (args: string[]): string =>
289
301
  execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
290
302
  const current = gitContext(cwd).branch;
291
- if (!current || current === "unknown") return { error: "not in a git repository" };
303
+ if (!current || current === "unknown")
304
+ return { error: "not in a git repository", phase: "preflight" };
292
305
  const sdd = sdd_dir ?? "docs";
293
306
  const manifestPath = path.isAbsolute(sdd)
294
307
  ? path.join(sdd, "manifest.json")
@@ -297,7 +310,6 @@ export const branchSetup = ({
297
310
  const dir = path.dirname(manifestPath);
298
311
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o755 });
299
312
  };
300
- ensureManifestDir();
301
313
  const readManifest = (): Record<string, unknown> => {
302
314
  try {
303
315
  return JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -315,13 +327,16 @@ export const branchSetup = ({
315
327
  if (action === "reapply_stash") {
316
328
  const manifest = readManifest();
317
329
  const ref = manifest.stash_ref;
318
- if (!ref) return { error: "no stash_ref in manifest" };
330
+ if (!ref) return { error: "no stash_ref in manifest", phase: "preflight" };
319
331
  journal(`pre-pop: ${String(ref)}`);
320
332
  try {
321
333
  exec(["stash", "pop", String(ref)]);
322
334
  } catch (error) {
323
335
  journal("pop: failed");
324
- return { error: error instanceof Error ? error.message : "stash pop failed" };
336
+ return {
337
+ error: error instanceof Error ? error.message : "stash pop failed",
338
+ phase: "post_mutation",
339
+ };
325
340
  }
326
341
  journal("pop: ok");
327
342
  delete manifest.stash_ref;
@@ -331,10 +346,17 @@ export const branchSetup = ({
331
346
  }
332
347
 
333
348
  const target = target_branch ?? "";
334
- if (!target) return { error: "target branch required" };
335
- if (isProtected(cwd, target)) return { error: `protected branch ${target}` };
349
+ if (!target)
350
+ return {
351
+ error: "target_branch (the working branch to create or switch to) is required for setup",
352
+ phase: "preflight",
353
+ };
354
+ if (isProtected(cwd, target)) return { error: `protected branch ${target}`, phase: "preflight" };
336
355
  if (!allowedBranch(cwd, target))
337
- return { error: `target branch ${target} is not allowed by the branch policy` };
356
+ return {
357
+ error: `target branch ${target} is not allowed by the branch policy`,
358
+ phase: "preflight",
359
+ };
338
360
 
339
361
  // CA-02: resolve the base up front so an unresolvable base fails before
340
362
  // any mutation. The origin/<base> validation runs after the stash gate
@@ -348,7 +370,7 @@ export const branchSetup = ({
348
370
  }
349
371
  if (!targetExists) {
350
372
  const baseResolved = baseBranch(cwd);
351
- if ("error" in baseResolved) return { error: baseResolved.error };
373
+ if ("error" in baseResolved) return { error: baseResolved.error, phase: "preflight" };
352
374
  base = baseResolved.base;
353
375
  }
354
376
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
@@ -356,7 +378,10 @@ export const branchSetup = ({
356
378
  let stash_ref: string | undefined;
357
379
  // Best-effort restore; if the pop itself fails, the caller's error gains a
358
380
  // suffix pointing at the stash so stranded work stays discoverable.
359
- const failAfterStash = (message: string): { error: string } => {
381
+ const failAfterStash = (
382
+ message: string,
383
+ phase: "preflight" | "post_mutation" = "post_mutation",
384
+ ): { error: string; phase: "preflight" | "post_mutation" } => {
360
385
  let suffix = "";
361
386
  if (stash_ref) {
362
387
  journal(`pre-pop: ${stash_ref}`);
@@ -369,7 +394,7 @@ export const branchSetup = ({
369
394
  suffix = " (changes preserved in stash)";
370
395
  }
371
396
  }
372
- return { error: `${message}${suffix}` };
397
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
373
398
  };
374
399
  if (current !== target) {
375
400
  const dirty = Boolean(gitContext(cwd).status_short.trim());
@@ -377,6 +402,7 @@ export const branchSetup = ({
377
402
  return {
378
403
  error:
379
404
  "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
405
+ phase: "preflight",
380
406
  };
381
407
  }
382
408
  // CA-02: validate origin/<base> before ANY mutation (no snapshot, no
@@ -386,14 +412,18 @@ export const branchSetup = ({
386
412
  let validatedBase: string | undefined;
387
413
  if (!targetExists && base !== undefined) {
388
414
  const ready = originBaseReady(cwd, base);
389
- if (!ready.ok) return { error: ready.error ?? "ensure-base-branch failed" };
415
+ if (!ready.ok)
416
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
390
417
  validatedBase = base;
391
418
  }
392
419
  if (dirty) {
393
420
  try {
394
421
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
395
422
  } catch (error) {
396
- return { error: error instanceof Error ? error.message : "stash push failed" };
423
+ return {
424
+ error: error instanceof Error ? error.message : "stash push failed",
425
+ phase: "post_mutation",
426
+ };
397
427
  }
398
428
  stash_ref = "stash@{0}";
399
429
  journal(`stash push: ${stash_ref}`);
@@ -406,13 +436,14 @@ export const branchSetup = ({
406
436
  if (/worktree/i.test(message)) {
407
437
  return failAfterStash(
408
438
  `branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`,
439
+ "preflight",
409
440
  );
410
441
  }
411
442
  try {
412
443
  let effectiveBase = base;
413
444
  if (effectiveBase === undefined) {
414
445
  const lateResolved = baseBranch(cwd);
415
- if ("error" in lateResolved) return failAfterStash(lateResolved.error);
446
+ if ("error" in lateResolved) return failAfterStash(lateResolved.error, "preflight");
416
447
  effectiveBase = lateResolved.base;
417
448
  }
418
449
  // Pre-validated above when the target was missing: only the
@@ -236,6 +236,112 @@ const safePath = (root: string, value: string): string | null => {
236
236
  }
237
237
  };
238
238
 
239
+ /** Read-only baseline bound into a branch-setup proposal: the exact remote
240
+ * commit the base currently points at, without moving any local ref. */
241
+ const branchCreationBaseline = (
242
+ root: string,
243
+ target: string,
244
+ base: string,
245
+ ): { base_branch: string; target_exists: boolean; remote_base: string | null } => {
246
+ const exists =
247
+ gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
248
+ if (exists) return { base_branch: base, target_exists: true, remote_base: null };
249
+ const remote = run(root, ["ls-remote", "origin", `refs/heads/${base}`]);
250
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
251
+ return {
252
+ base_branch: base,
253
+ target_exists: false,
254
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? (sha as string) : null,
255
+ };
256
+ };
257
+
258
+ const shortSha = (value: unknown): string =>
259
+ typeof value === "string" && value ? value.slice(0, 8) : "unknown";
260
+
261
+ const isProtectedBranch = (root: string, branch: string): boolean =>
262
+ resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
263
+
264
+ /** Concise, user-facing approval text for one resolved action. The exact
265
+ * descriptor stays machine-facing; this is what the native question shows. */
266
+ export const actionProposalQuestion = (
267
+ request: ExternalActionRequest,
268
+ descriptorPayload: unknown,
269
+ ): { presented: string; approvedText: string } => {
270
+ const payload = (descriptorPayload ?? {}) as Record<string, unknown>;
271
+ const resolved = (payload.resolved ?? {}) as Record<string, unknown>;
272
+ const payloadTarget = String(payload.target_branch ?? "");
273
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
274
+ const steps = payload.plan_steps as string[];
275
+ return {
276
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
277
+ approvedText: `Approve the ${steps.length} listed plan commits.`,
278
+ };
279
+ }
280
+ switch (request.operation) {
281
+ case "git.branch_setup": {
282
+ if (request.payload.action === "reapply_stash")
283
+ return {
284
+ presented:
285
+ "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
286
+ approvedText: "Reapply the recorded stash.",
287
+ };
288
+ const target = String(payload.target_branch ?? "");
289
+ if (resolved.target_exists === true)
290
+ return {
291
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
292
+ approvedText: `Switch to \`${target}\`.`,
293
+ };
294
+ const base = String(resolved.base_branch ?? "the base branch");
295
+ return {
296
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
297
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`,
298
+ };
299
+ }
300
+ case "git.commit": {
301
+ const paths = Array.isArray(resolved.paths) ? (resolved.paths as string[]) : [];
302
+ return {
303
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
304
+ approvedText: `Commit as "${String(request.payload.message)}".`,
305
+ };
306
+ }
307
+ case "git.push":
308
+ return {
309
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
310
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`,
311
+ };
312
+ case "hosting.pull_request":
313
+ return {
314
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
315
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`,
316
+ };
317
+ case "changelog.apply":
318
+ return {
319
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
320
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`,
321
+ };
322
+ case "youtrack.update":
323
+ return {
324
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
325
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`,
326
+ };
327
+ case "youtrack.time":
328
+ return {
329
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
330
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`,
331
+ };
332
+ case "youtrack.meeting":
333
+ return {
334
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
335
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`,
336
+ };
337
+ default:
338
+ return {
339
+ presented: `Workit decision: action — Approve ${request.operation}?`,
340
+ approvedText: `Approve ${request.operation}.`,
341
+ };
342
+ }
343
+ };
344
+
239
345
  export type ResolvedExternalAction = {
240
346
  request: ExternalActionRequest;
241
347
  descriptorPayload: unknown;
@@ -697,23 +803,41 @@ export const resolveExternalActionRequest = (
697
803
  case "git.branch_setup": {
698
804
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
699
805
  if (!sdd_dir) return failure("invalid_input", "sdd_dir must stay inside the workspace");
806
+ const action = request.payload.action ?? "setup";
700
807
  const policy = resolveBranchPolicyFor(root);
701
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
808
+ const target_branch = request.payload.target_branch;
809
+ if (action !== "reapply_stash" && !target_branch)
810
+ return failure(
811
+ "invalid_input",
812
+ "target_branch (the working branch to create or switch to) is required for setup",
813
+ {
814
+ outcome: "not_started",
815
+ fields: [{ path: "target_branch", reason: "required for action=setup" }],
816
+ },
817
+ );
702
818
  const head = gitValue(root, ["rev-parse", "HEAD"]);
703
819
  if (!head) return failure("storage_error", "current Git commit could not be resolved");
820
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
821
+ const creation =
822
+ action === "reapply_stash" || !target_branch
823
+ ? { base_branch: policy.defaultTargetBranch }
824
+ : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
704
825
  const normalized = {
705
826
  operation: request.operation,
706
827
  payload: {
707
828
  ...request.payload,
708
- action: request.payload.action ?? "setup",
829
+ action,
709
830
  sdd_dir,
710
831
  stash: request.payload.stash ?? "no",
711
- target_branch,
832
+ ...(target_branch ? { target_branch } : {}),
712
833
  },
713
834
  } as ExternalActionRequest;
714
835
  return success(null, null, {
715
836
  request: normalized,
716
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } },
837
+ descriptorPayload: {
838
+ ...normalized.payload,
839
+ resolved: { head, dirty, ...creation },
840
+ },
717
841
  });
718
842
  }
719
843
  case "git.push": {
@@ -729,7 +853,46 @@ export const resolveExternalActionRequest = (
729
853
  : failure("invalid_input", "git push requires a resolvable current branch");
730
854
  }
731
855
  case "git.commit": {
856
+ const planSteps = request.payload.plan_steps;
857
+ const planBranch = request.payload.plan_branch;
732
858
  const head = gitValue(root, ["rev-parse", "HEAD"]);
859
+ if (!head) return failure("storage_error", "Git state could not be resolved");
860
+ const branch = gitValue(root, ["branch", "--show-current"]);
861
+ if (planSteps !== undefined || planBranch !== undefined) {
862
+ if (
863
+ !Array.isArray(planSteps) ||
864
+ planSteps.some((step) => typeof step !== "string" || !step.trim()) ||
865
+ new Set(planSteps).size !== planSteps.length ||
866
+ planSteps.length === 0 ||
867
+ planSteps.length > 32
868
+ )
869
+ return failure(
870
+ "invalid_input",
871
+ "plan_steps must be 1-32 unique non-empty commit messages",
872
+ {
873
+ outcome: "not_started",
874
+ fields: [{ path: "plan_steps", reason: "unique strings" }],
875
+ },
876
+ );
877
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
878
+ return failure("invalid_input", "plan_branch must name the current working branch", {
879
+ outcome: "not_started",
880
+ fields: [{ path: "plan_branch", reason: "current branch" }],
881
+ });
882
+ if (isProtectedBranch(root, planBranch))
883
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
884
+ outcome: "not_started",
885
+ fields: [{ path: "plan_branch", reason: "protected branch" }],
886
+ });
887
+ return success(null, null, {
888
+ request,
889
+ descriptorPayload: {
890
+ plan_steps: planSteps,
891
+ plan_branch: planBranch,
892
+ resolved: { head, branch, steps: planSteps },
893
+ },
894
+ });
895
+ }
733
896
  const staged = gitRaw(root, [
734
897
  "diff",
735
898
  "--cached",
@@ -740,7 +903,13 @@ export const resolveExternalActionRequest = (
740
903
  "HEAD",
741
904
  ]);
742
905
  const paths = stagedPaths(root);
743
- if (!head || staged === null || paths === null)
906
+ const message = request.payload.message;
907
+ if (typeof message !== "string" || !message)
908
+ return failure("invalid_input", "git.commit requires a commit message", {
909
+ outcome: "not_started",
910
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }],
911
+ });
912
+ if (staged === null || paths === null)
744
913
  return failure("storage_error", "Git state could not be resolved");
745
914
  let policy: { preset: string; pattern?: string };
746
915
  try {
@@ -757,7 +926,7 @@ export const resolveExternalActionRequest = (
757
926
  const detected = detectCommitFlavor((log ?? "").split("\n").filter(Boolean));
758
927
  flavor = detected.flavor ?? "conventional";
759
928
  }
760
- if (!matchCommitFlavor(request.payload.message, flavor as CommitFlavor, policy.pattern))
929
+ if (!matchCommitFlavor(message, flavor as CommitFlavor, policy.pattern))
761
930
  return failure(
762
931
  "invalid_input",
763
932
  `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`,
@@ -766,7 +935,7 @@ export const resolveExternalActionRequest = (
766
935
  request,
767
936
  descriptorPayload: {
768
937
  ...request.payload,
769
- resolved: { head, staged: sha256(staged), paths },
938
+ resolved: { head, branch, staged: sha256(staged), paths },
770
939
  },
771
940
  });
772
941
  }
@@ -985,10 +1154,24 @@ export const executeConcreteExternalAction = async (
985
1154
  sdd_dir: request.payload.sdd_dir ?? "docs",
986
1155
  workspace_root: root,
987
1156
  });
988
- return result.error ? unknown(request.operation) : success(null, null, result);
1157
+ if ("error" in result) {
1158
+ if (result.phase === "preflight")
1159
+ return failure("invalid_input", result.error, {
1160
+ outcome: "not_started",
1161
+ operation: request.operation,
1162
+ });
1163
+ return unknown(request.operation);
1164
+ }
1165
+ return success(null, null, result);
989
1166
  }
990
1167
  case "git.commit": {
991
- const result = run(root, ["commit", "-m", request.payload.message]);
1168
+ const message = request.payload.message;
1169
+ if (typeof message !== "string" || !message)
1170
+ return failure("invalid_input", "git.commit requires a commit message", {
1171
+ outcome: "not_started",
1172
+ fields: [{ path: "message", reason: "required" }],
1173
+ });
1174
+ const result = run(root, ["commit", "-m", message]);
992
1175
  return result.exitCode === 0
993
1176
  ? success(null, null, { stdout: result.stdout.trim() })
994
1177
  : unknown(request.operation);