@gajae-code/coding-agent 0.4.4 → 0.5.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.
Files changed (132) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/types/cli/fast-help.d.ts +1 -0
  3. package/dist/types/cli/setup-cli.d.ts +2 -0
  4. package/dist/types/commands/harness.d.ts +6 -0
  5. package/dist/types/commands/setup.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +11 -2
  7. package/dist/types/config/model-profiles.d.ts +7 -0
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/model-resolver.d.ts +2 -0
  10. package/dist/types/config/models-config-schema.d.ts +35 -0
  11. package/dist/types/config/settings-schema.d.ts +4 -3
  12. package/dist/types/coordinator/contract.d.ts +1 -1
  13. package/dist/types/coordinator-mcp/server.d.ts +8 -2
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  16. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  21. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  22. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  23. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  24. package/dist/types/harness-control-plane/types.d.ts +13 -1
  25. package/dist/types/hindsight/mental-models.d.ts +5 -5
  26. package/dist/types/main.d.ts +2 -2
  27. package/dist/types/modes/components/model-selector.d.ts +1 -12
  28. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  29. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  30. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  31. package/dist/types/sdk.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +2 -0
  33. package/dist/types/session/blob-store.d.ts +20 -1
  34. package/dist/types/session/session-manager.d.ts +32 -6
  35. package/dist/types/session/streaming-output.d.ts +3 -2
  36. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  37. package/dist/types/setup/hermes-setup.d.ts +7 -0
  38. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  39. package/dist/types/task/receipt.d.ts +2 -0
  40. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  41. package/dist/types/task/types.d.ts +17 -0
  42. package/dist/types/thinking-metadata.d.ts +16 -0
  43. package/dist/types/thinking.d.ts +3 -12
  44. package/dist/types/tools/index.d.ts +2 -0
  45. package/dist/types/tools/resolve.d.ts +0 -10
  46. package/dist/types/utils/tool-choice.d.ts +14 -1
  47. package/package.json +8 -7
  48. package/scripts/build-binary.ts +4 -0
  49. package/src/cli/fast-help.ts +80 -0
  50. package/src/cli/setup-cli.ts +12 -3
  51. package/src/cli.ts +112 -17
  52. package/src/commands/coordinator.ts +44 -1
  53. package/src/commands/harness.ts +128 -11
  54. package/src/commands/launch.ts +2 -2
  55. package/src/commands/mcp-serve.ts +3 -2
  56. package/src/commands/session.ts +3 -1
  57. package/src/commands/setup.ts +4 -0
  58. package/src/config/model-profile-activation.ts +15 -3
  59. package/src/config/model-profiles.ts +255 -56
  60. package/src/config/model-resolver.ts +9 -6
  61. package/src/config/models-config-schema.ts +2 -0
  62. package/src/config/settings-schema.ts +6 -3
  63. package/src/coordinator/contract.ts +1 -0
  64. package/src/coordinator-mcp/server.ts +427 -193
  65. package/src/cursor.ts +46 -4
  66. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  67. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  68. package/src/export/html/index.ts +13 -9
  69. package/src/gjc-runtime/launch-worktree.ts +12 -1
  70. package/src/gjc-runtime/session-state-sidecar.ts +38 -0
  71. package/src/gjc-runtime/team-runtime.ts +33 -7
  72. package/src/gjc-runtime/tmux-common.ts +15 -0
  73. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  74. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  75. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  76. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  77. package/src/harness-control-plane/finalize.ts +39 -5
  78. package/src/harness-control-plane/owner.ts +87 -28
  79. package/src/harness-control-plane/phase-rollup.ts +96 -0
  80. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  81. package/src/harness-control-plane/receipt-spool.ts +128 -0
  82. package/src/harness-control-plane/receipts.ts +229 -1
  83. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  84. package/src/harness-control-plane/state-machine.ts +27 -6
  85. package/src/harness-control-plane/storage.ts +23 -0
  86. package/src/harness-control-plane/types.ts +33 -1
  87. package/src/hindsight/mental-models.ts +17 -16
  88. package/src/internal-urls/docs-index.generated.ts +8 -7
  89. package/src/main.ts +7 -3
  90. package/src/modes/components/assistant-message.ts +26 -14
  91. package/src/modes/components/diff.ts +97 -0
  92. package/src/modes/components/model-selector.ts +353 -181
  93. package/src/modes/components/status-line.ts +6 -6
  94. package/src/modes/components/tool-execution.ts +30 -13
  95. package/src/modes/controllers/event-controller.ts +5 -4
  96. package/src/modes/controllers/selector-controller.ts +33 -42
  97. package/src/modes/interactive-mode.ts +4 -5
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +3 -2
  100. package/src/modes/rpc/rpc-mode.ts +44 -14
  101. package/src/modes/rpc/rpc-types.ts +5 -2
  102. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  103. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  104. package/src/modes/theme/theme.ts +2 -2
  105. package/src/modes/utils/abort-message.ts +41 -0
  106. package/src/modes/utils/context-usage.ts +15 -8
  107. package/src/modes/utils/ui-helpers.ts +5 -6
  108. package/src/sdk.ts +38 -6
  109. package/src/secrets/obfuscator.ts +102 -27
  110. package/src/session/agent-session.ts +121 -25
  111. package/src/session/blob-store.ts +89 -3
  112. package/src/session/session-manager.ts +328 -57
  113. package/src/session/streaming-output.ts +185 -122
  114. package/src/session/tool-choice-queue.ts +23 -0
  115. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
  116. package/src/setup/hermes-setup.ts +63 -8
  117. package/src/task/executor.ts +69 -6
  118. package/src/task/fork-context-advisory.ts +99 -0
  119. package/src/task/index.ts +31 -2
  120. package/src/task/receipt.ts +7 -0
  121. package/src/task/render.ts +21 -1
  122. package/src/task/roi-reconciliation.ts +90 -0
  123. package/src/task/types.ts +15 -0
  124. package/src/thinking-metadata.ts +51 -0
  125. package/src/thinking.ts +26 -46
  126. package/src/tools/bash.ts +1 -1
  127. package/src/tools/index.ts +4 -2
  128. package/src/tools/resolve.ts +93 -18
  129. package/src/tools/subagent-render.ts +10 -1
  130. package/src/utils/edit-mode.ts +1 -1
  131. package/src/utils/title-generator.ts +16 -2
  132. package/src/utils/tool-choice.ts +45 -16
@@ -116,6 +116,27 @@ const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
116
116
  const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
117
117
 
118
118
  const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
119
+ const NATIVE_STEERING_KINDS = [
120
+ "add_subgoal",
121
+ "split_subgoal",
122
+ "reorder_pending",
123
+ "revise_pending_wording",
124
+ "annotate_ledger",
125
+ "mark_blocked_superseded",
126
+ ] as const;
127
+ type UltragoalSteeringKind = (typeof NATIVE_STEERING_KINDS)[number];
128
+ const NATIVE_STEERING_KIND_SET = new Set<string>(NATIVE_STEERING_KINDS);
129
+
130
+ interface ReplacementSpec {
131
+ title: string;
132
+ objective: string;
133
+ }
134
+
135
+ interface SteeringCommandResult {
136
+ kind: UltragoalSteeringKind;
137
+ message: string;
138
+ receipt: JsonObject;
139
+ }
119
140
 
120
141
  function stableStructuredValue(value: unknown): unknown {
121
142
  if (Array.isArray(value)) return value.map(item => stableStructuredValue(item));
@@ -1263,44 +1284,372 @@ export async function checkpointAndContinueUltragoalGoal(input: {
1263
1284
  };
1264
1285
  }
1265
1286
 
1266
- export async function addUltragoalSubgoal(input: {
1287
+ function nextUltragoalGoalId(plan: UltragoalPlan, offset = 1): string {
1288
+ return `G${String(plan.goals.length + offset).padStart(3, "0")}`;
1289
+ }
1290
+
1291
+ function requireSteeringText(value: string, label: string, kind: UltragoalSteeringKind): string {
1292
+ const trimmed = value.trim();
1293
+ if (!trimmed) throw new Error(`steer --${label} is required for ${kind}`);
1294
+ return trimmed;
1295
+ }
1296
+
1297
+ function requireSteeringEvidence(input: { kind: UltragoalSteeringKind; evidence: string; rationale: string }): {
1298
+ evidence: string;
1299
+ rationale: string;
1300
+ } {
1301
+ return {
1302
+ evidence: requireSteeringText(input.evidence, "evidence", input.kind),
1303
+ rationale: requireSteeringText(input.rationale, "rationale", input.kind),
1304
+ };
1305
+ }
1306
+
1307
+ function findGoalOrThrow(plan: UltragoalPlan, goalId: string, kind: UltragoalSteeringKind): UltragoalGoal {
1308
+ const id = goalId.trim();
1309
+ if (!id) throw new Error(`steer --goal-id is required for ${kind}`);
1310
+ const goal = plan.goals.find(item => item.id === id);
1311
+ if (!goal) throw new Error(`No ultragoal goal found for ${id}.`);
1312
+ return goal;
1313
+ }
1314
+
1315
+ function requireGoalStatus(
1316
+ goal: UltragoalGoal,
1317
+ allowed: readonly UltragoalGoalStatus[],
1318
+ kind: UltragoalSteeringKind,
1319
+ ): void {
1320
+ if (!allowed.includes(goal.status)) {
1321
+ throw new Error(`steer ${kind} requires goal ${goal.id} status ${allowed.join(" or ")}; found ${goal.status}`);
1322
+ }
1323
+ }
1324
+
1325
+ function parseJsonFlag(value: string, label: string, kind: UltragoalSteeringKind): unknown {
1326
+ const trimmed = requireSteeringText(value, label, kind);
1327
+ try {
1328
+ return JSON.parse(trimmed) as unknown;
1329
+ } catch (error) {
1330
+ const message = error instanceof Error ? error.message : String(error);
1331
+ throw new Error(`steer --${label} must be valid JSON for ${kind}: ${message}`);
1332
+ }
1333
+ }
1334
+
1335
+ function parseReplacementSpecs(value: string, kind: UltragoalSteeringKind): ReplacementSpec[] {
1336
+ const raw = parseJsonFlag(value, "replacements-json", kind);
1337
+ if (!Array.isArray(raw) || raw.length < 2) {
1338
+ throw new Error("steer --replacements-json must be an array with at least two replacements");
1339
+ }
1340
+ const seen = new Set<string>();
1341
+ return raw.map((item, index) => {
1342
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
1343
+ throw new Error(`steer --replacements-json[${index}] must be an object`);
1344
+ }
1345
+ const record = item as Record<string, unknown>;
1346
+ const title = typeof record.title === "string" ? record.title.trim() : "";
1347
+ const objective = typeof record.objective === "string" ? record.objective.trim() : "";
1348
+ if (!title || !objective) {
1349
+ throw new Error(`steer --replacements-json[${index}] requires non-empty title and objective`);
1350
+ }
1351
+ const key = `${title}\u0000${objective}`;
1352
+ if (seen.has(key)) throw new Error(`steer --replacements-json[${index}] duplicates an earlier replacement`);
1353
+ seen.add(key);
1354
+ return { title, objective };
1355
+ });
1356
+ }
1357
+
1358
+ function parsePendingOrder(value: string, kind: UltragoalSteeringKind): string[] {
1359
+ const raw = parseJsonFlag(value, "order-json", kind);
1360
+ if (!Array.isArray(raw) || raw.length === 0) {
1361
+ throw new Error("steer --order-json must be a non-empty array of goal ids");
1362
+ }
1363
+ const seen = new Set<string>();
1364
+ return raw.map((item, index) => {
1365
+ if (typeof item !== "string" || item.trim().length === 0) {
1366
+ throw new Error(`steer --order-json[${index}] must be a non-empty goal id string`);
1367
+ }
1368
+ const id = item.trim();
1369
+ if (seen.has(id)) throw new Error(`steer --order-json contains duplicate goal id ${id}`);
1370
+ seen.add(id);
1371
+ return id;
1372
+ });
1373
+ }
1374
+
1375
+ async function appendSteeringRejected(input: {
1376
+ cwd: string;
1377
+ kind: UltragoalSteeringKind;
1378
+ reason: string;
1379
+ goalId?: string;
1380
+ evidence?: string;
1381
+ rationale?: string;
1382
+ payload?: JsonObject;
1383
+ }): Promise<void> {
1384
+ await appendLedger(input.cwd, {
1385
+ event: "steering_rejected",
1386
+ kind: input.kind,
1387
+ goalId: input.goalId?.trim() || undefined,
1388
+ reason: input.reason,
1389
+ evidence: input.evidence?.trim() || undefined,
1390
+ rationale: input.rationale?.trim() || undefined,
1391
+ payload: input.payload,
1392
+ });
1393
+ }
1394
+
1395
+ function steeringPayloadSummary(args: readonly string[]): JsonObject {
1396
+ return {
1397
+ goalId: flagValue(args, "--goal-id"),
1398
+ title: flagValue(args, "--title"),
1399
+ objective: flagValue(args, "--objective"),
1400
+ replacementsJson: flagValue(args, "--replacements-json"),
1401
+ orderJson: flagValue(args, "--order-json"),
1402
+ };
1403
+ }
1404
+
1405
+ function parseNativeSteeringKind(value: string | undefined): UltragoalSteeringKind {
1406
+ if (typeof value === "string" && NATIVE_STEERING_KIND_SET.has(value)) return value as UltragoalSteeringKind;
1407
+ throw new Error(`native steering currently supports --kind ${NATIVE_STEERING_KINDS.join(", ")}`);
1408
+ }
1409
+
1410
+ async function addUltragoalSubgoalToPlan(input: {
1267
1411
  cwd: string;
1412
+ plan: UltragoalPlan;
1268
1413
  title: string;
1269
1414
  objective: string;
1270
1415
  evidence: string;
1271
1416
  rationale: string;
1272
- }): Promise<UltragoalPlan> {
1273
- const plan = await readUltragoalPlan(input.cwd);
1274
- if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
1275
- for (const [label, value] of [
1276
- ["title", input.title],
1277
- ["objective", input.objective],
1278
- ["evidence", input.evidence],
1279
- ["rationale", input.rationale],
1280
- ] as const) {
1281
- if (!value.trim()) throw new Error(`steer --${label} is required for add_subgoal`);
1282
- }
1417
+ }): Promise<{ plan: UltragoalPlan; goalId: string }> {
1418
+ const kind = "add_subgoal";
1419
+ const title = requireSteeringText(input.title, "title", kind);
1420
+ const objective = requireSteeringText(input.objective, "objective", kind);
1421
+ const { evidence, rationale } = requireSteeringEvidence({
1422
+ kind,
1423
+ evidence: input.evidence,
1424
+ rationale: input.rationale,
1425
+ });
1283
1426
  const now = new Date().toISOString();
1284
- const nextId = `G${String(plan.goals.length + 1).padStart(3, "0")}`;
1285
- plan.goals.push({
1427
+ const nextId = nextUltragoalGoalId(input.plan);
1428
+ input.plan.goals.push({
1286
1429
  id: nextId,
1287
- title: input.title.trim(),
1288
- objective: input.objective.trim(),
1430
+ title,
1431
+ objective,
1289
1432
  status: "pending",
1290
1433
  createdAt: now,
1291
1434
  updatedAt: now,
1292
- steering: { kind: "add_subgoal", evidence: input.evidence.trim(), rationale: input.rationale.trim() },
1435
+ steering: { kind, evidence, rationale },
1293
1436
  });
1294
- plan.updatedAt = now;
1295
- await writePlan(input.cwd, plan);
1437
+ input.plan.updatedAt = now;
1438
+ await writePlan(input.cwd, input.plan);
1296
1439
  await appendLedger(input.cwd, {
1297
1440
  event: "steering_accepted",
1298
- kind: "add_subgoal",
1441
+ kind,
1299
1442
  goalId: nextId,
1300
- evidence: input.evidence.trim(),
1301
- rationale: input.rationale.trim(),
1443
+ evidence,
1444
+ rationale,
1302
1445
  });
1303
- return plan;
1446
+ return { plan: input.plan, goalId: nextId };
1447
+ }
1448
+
1449
+ export async function addUltragoalSubgoal(input: {
1450
+ cwd: string;
1451
+ title: string;
1452
+ objective: string;
1453
+ evidence: string;
1454
+ rationale: string;
1455
+ }): Promise<UltragoalPlan> {
1456
+ const plan = await readUltragoalPlan(input.cwd);
1457
+ if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
1458
+ return (await addUltragoalSubgoalToPlan({ ...input, plan })).plan;
1459
+ }
1460
+
1461
+ async function splitUltragoalSubgoal(input: {
1462
+ cwd: string;
1463
+ plan: UltragoalPlan;
1464
+ goalId: string;
1465
+ replacementsJson: string;
1466
+ evidence: string;
1467
+ rationale: string;
1468
+ }): Promise<{ plan: UltragoalPlan; goalId: string; replacementGoalIds: string[] }> {
1469
+ const kind = "split_subgoal";
1470
+ const { evidence, rationale } = requireSteeringEvidence({
1471
+ kind,
1472
+ evidence: input.evidence,
1473
+ rationale: input.rationale,
1474
+ });
1475
+ const target = findGoalOrThrow(input.plan, input.goalId, kind);
1476
+ requireGoalStatus(target, ["pending"], kind);
1477
+ const replacements = parseReplacementSpecs(input.replacementsJson, kind);
1478
+ const now = new Date().toISOString();
1479
+ const replacementGoalIds = replacements.map((_, index) => nextUltragoalGoalId(input.plan, index + 1));
1480
+ target.status = "superseded";
1481
+ target.evidence = evidence;
1482
+ target.updatedAt = now;
1483
+ target.steering = { kind, evidence, rationale, replacementGoalIds };
1484
+ const replacementGoals = replacements.map(
1485
+ (replacement, index): UltragoalGoal => ({
1486
+ id: replacementGoalIds[index]!,
1487
+ title: replacement.title,
1488
+ objective: replacement.objective,
1489
+ status: "pending",
1490
+ createdAt: now,
1491
+ updatedAt: now,
1492
+ steering: { kind: "split_replacement", sourceGoalId: target.id, evidence, rationale },
1493
+ }),
1494
+ );
1495
+ const targetIndex = input.plan.goals.findIndex(goal => goal.id === target.id);
1496
+ input.plan.goals.splice(targetIndex + 1, 0, ...replacementGoals);
1497
+ input.plan.updatedAt = now;
1498
+ await writePlan(input.cwd, input.plan);
1499
+ await appendLedger(input.cwd, {
1500
+ event: "steering_accepted",
1501
+ kind,
1502
+ goalId: target.id,
1503
+ replacementGoalIds,
1504
+ evidence,
1505
+ rationale,
1506
+ });
1507
+ return { plan: input.plan, goalId: target.id, replacementGoalIds };
1508
+ }
1509
+
1510
+ async function reorderPendingUltragoalGoals(input: {
1511
+ cwd: string;
1512
+ plan: UltragoalPlan;
1513
+ orderJson: string;
1514
+ evidence: string;
1515
+ rationale: string;
1516
+ }): Promise<{ plan: UltragoalPlan; pendingGoalIds: string[] }> {
1517
+ const kind = "reorder_pending";
1518
+ const { evidence, rationale } = requireSteeringEvidence({
1519
+ kind,
1520
+ evidence: input.evidence,
1521
+ rationale: input.rationale,
1522
+ });
1523
+ const pendingGoalIds = input.plan.goals.filter(goal => goal.status === "pending").map(goal => goal.id);
1524
+ const requestedOrder = parsePendingOrder(input.orderJson, kind);
1525
+ const pendingSet = new Set(pendingGoalIds);
1526
+ for (const id of requestedOrder) {
1527
+ const goal = input.plan.goals.find(item => item.id === id);
1528
+ if (!goal) throw new Error(`steer --order-json references unknown goal id ${id}`);
1529
+ if (goal.status !== "pending") throw new Error(`steer --order-json references non-pending goal id ${id}`);
1530
+ }
1531
+ const missing = pendingGoalIds.filter(id => !requestedOrder.includes(id));
1532
+ if (missing.length > 0) throw new Error(`steer --order-json missing pending goal id(s): ${missing.join(", ")}`);
1533
+ if (requestedOrder.length !== pendingSet.size)
1534
+ throw new Error("steer --order-json must include every pending goal exactly once");
1535
+ const pendingById = new Map(input.plan.goals.map(goal => [goal.id, goal]));
1536
+ const remaining = [...requestedOrder];
1537
+ input.plan.goals = input.plan.goals.map(goal =>
1538
+ goal.status === "pending" ? pendingById.get(remaining.shift()!)! : goal,
1539
+ );
1540
+ input.plan.updatedAt = new Date().toISOString();
1541
+ await writePlan(input.cwd, input.plan);
1542
+ await appendLedger(input.cwd, {
1543
+ event: "steering_accepted",
1544
+ kind,
1545
+ previousPendingGoalIds: pendingGoalIds,
1546
+ pendingGoalIds: requestedOrder,
1547
+ evidence,
1548
+ rationale,
1549
+ });
1550
+ return { plan: input.plan, pendingGoalIds: requestedOrder };
1551
+ }
1552
+
1553
+ async function revisePendingUltragoalWording(input: {
1554
+ cwd: string;
1555
+ plan: UltragoalPlan;
1556
+ goalId: string;
1557
+ title?: string;
1558
+ objective?: string;
1559
+ evidence: string;
1560
+ rationale: string;
1561
+ }): Promise<{ plan: UltragoalPlan; goalId: string; changedFields: string[] }> {
1562
+ const kind = "revise_pending_wording";
1563
+ const { evidence, rationale } = requireSteeringEvidence({
1564
+ kind,
1565
+ evidence: input.evidence,
1566
+ rationale: input.rationale,
1567
+ });
1568
+ const goal = findGoalOrThrow(input.plan, input.goalId, kind);
1569
+ requireGoalStatus(goal, ["pending"], kind);
1570
+ const title = input.title === undefined ? undefined : input.title.trim();
1571
+ const objective = input.objective === undefined ? undefined : input.objective.trim();
1572
+ if (input.title !== undefined && !title)
1573
+ throw new Error("steer --title must be non-empty for revise_pending_wording");
1574
+ if (input.objective !== undefined && !objective)
1575
+ throw new Error("steer --objective must be non-empty for revise_pending_wording");
1576
+ if (!title && !objective) throw new Error("revise_pending_wording requires --title and/or --objective");
1577
+ const changedFields: string[] = [];
1578
+ if (title !== undefined) {
1579
+ goal.title = title;
1580
+ changedFields.push("title");
1581
+ }
1582
+ if (objective !== undefined) {
1583
+ goal.objective = objective;
1584
+ changedFields.push("objective");
1585
+ }
1586
+ const now = new Date().toISOString();
1587
+ goal.updatedAt = now;
1588
+ goal.steering = { kind, evidence, rationale, changedFields };
1589
+ input.plan.updatedAt = now;
1590
+ await writePlan(input.cwd, input.plan);
1591
+ await appendLedger(input.cwd, {
1592
+ event: "steering_accepted",
1593
+ kind,
1594
+ goalId: goal.id,
1595
+ changedFields,
1596
+ evidence,
1597
+ rationale,
1598
+ });
1599
+ return { plan: input.plan, goalId: goal.id, changedFields };
1600
+ }
1601
+
1602
+ async function annotateUltragoalLedger(input: {
1603
+ cwd: string;
1604
+ plan: UltragoalPlan;
1605
+ evidence: string;
1606
+ rationale: string;
1607
+ }): Promise<{ plan: UltragoalPlan }> {
1608
+ const kind = "annotate_ledger";
1609
+ const { evidence, rationale } = requireSteeringEvidence({
1610
+ kind,
1611
+ evidence: input.evidence,
1612
+ rationale: input.rationale,
1613
+ });
1614
+ await appendLedger(input.cwd, { event: "steering_accepted", kind, evidence, rationale });
1615
+ return { plan: input.plan };
1616
+ }
1617
+
1618
+ async function markBlockedUltragoalSuperseded(input: {
1619
+ cwd: string;
1620
+ plan: UltragoalPlan;
1621
+ goalId: string;
1622
+ evidence: string;
1623
+ rationale: string;
1624
+ }): Promise<{ plan: UltragoalPlan; goalId: string }> {
1625
+ const kind = "mark_blocked_superseded";
1626
+ const { evidence, rationale } = requireSteeringEvidence({
1627
+ kind,
1628
+ evidence: input.evidence,
1629
+ rationale: input.rationale,
1630
+ });
1631
+ const goal = findGoalOrThrow(input.plan, input.goalId, kind);
1632
+ requireGoalStatus(goal, ["blocked", "review_blocked"], kind);
1633
+ const remainingRequiredGoals = requiredUltragoalGoals(input.plan).filter(item => item.id !== goal.id);
1634
+ if (remainingRequiredGoals.length === 0) {
1635
+ throw new Error(`steer ${kind} cannot supersede ${goal.id} because it is the only remaining required goal`);
1636
+ }
1637
+ const now = new Date().toISOString();
1638
+ goal.status = "superseded";
1639
+ goal.evidence = evidence;
1640
+ goal.updatedAt = now;
1641
+ goal.steering = { kind, evidence, rationale, noReplacementRequired: true };
1642
+ input.plan.updatedAt = now;
1643
+ await writePlan(input.cwd, input.plan);
1644
+ await appendLedger(input.cwd, {
1645
+ event: "steering_accepted",
1646
+ kind,
1647
+ goalId: goal.id,
1648
+ noReplacementRequired: true,
1649
+ evidence,
1650
+ rationale,
1651
+ });
1652
+ return { plan: input.plan, goalId: goal.id };
1304
1653
  }
1305
1654
 
1306
1655
  export async function recordUltragoalReviewBlockers(input: {
@@ -1365,6 +1714,8 @@ const FLAGS_WITH_VALUES = new Set([
1365
1714
  "--title",
1366
1715
  "--objective",
1367
1716
  "--rationale",
1717
+ "--replacements-json",
1718
+ "--order-json",
1368
1719
  ]);
1369
1720
 
1370
1721
  function isHelpArg(arg: string): boolean {
@@ -1523,6 +1874,135 @@ function renderCheckpointContinuation(
1523
1874
  return lines.join("\n");
1524
1875
  }
1525
1876
 
1877
+ async function executeUltragoalSteeringCommand(args: readonly string[], cwd: string): Promise<SteeringCommandResult> {
1878
+ const kind = parseNativeSteeringKind(flagValue(args, "--kind"));
1879
+ const plan = await readUltragoalPlan(cwd);
1880
+ if (!plan) throw new Error("No ultragoal plan found. Run `gjc ultragoal create-goals --brief ...` first.");
1881
+ const evidence = flagValue(args, "--evidence") ?? "";
1882
+ const rationale = flagValue(args, "--rationale") ?? "";
1883
+ try {
1884
+ switch (kind) {
1885
+ case "add_subgoal": {
1886
+ const result = await addUltragoalSubgoalToPlan({
1887
+ cwd,
1888
+ plan,
1889
+ title: flagValue(args, "--title") ?? "",
1890
+ objective: flagValue(args, "--objective") ?? "",
1891
+ evidence,
1892
+ rationale,
1893
+ });
1894
+ return {
1895
+ kind,
1896
+ message: "Accepted add_subgoal steering.\n",
1897
+ receipt: { ok: true, kind, goal_id: result.goalId, goals_path: getUltragoalPaths(cwd).goalsPath },
1898
+ };
1899
+ }
1900
+ case "split_subgoal": {
1901
+ const result = await splitUltragoalSubgoal({
1902
+ cwd,
1903
+ plan,
1904
+ goalId: flagValue(args, "--goal-id") ?? "",
1905
+ replacementsJson: flagValue(args, "--replacements-json") ?? "",
1906
+ evidence,
1907
+ rationale,
1908
+ });
1909
+ return {
1910
+ kind,
1911
+ message: "Accepted split_subgoal steering.\n",
1912
+ receipt: {
1913
+ ok: true,
1914
+ kind,
1915
+ goal_id: result.goalId,
1916
+ replacement_goal_ids: result.replacementGoalIds,
1917
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1918
+ },
1919
+ };
1920
+ }
1921
+ case "reorder_pending": {
1922
+ const result = await reorderPendingUltragoalGoals({
1923
+ cwd,
1924
+ plan,
1925
+ orderJson: flagValue(args, "--order-json") ?? "",
1926
+ evidence,
1927
+ rationale,
1928
+ });
1929
+ return {
1930
+ kind,
1931
+ message: "Accepted reorder_pending steering.\n",
1932
+ receipt: {
1933
+ ok: true,
1934
+ kind,
1935
+ pending_goal_ids: result.pendingGoalIds,
1936
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1937
+ },
1938
+ };
1939
+ }
1940
+ case "revise_pending_wording": {
1941
+ const result = await revisePendingUltragoalWording({
1942
+ cwd,
1943
+ plan,
1944
+ goalId: flagValue(args, "--goal-id") ?? "",
1945
+ title: flagValue(args, "--title"),
1946
+ objective: flagValue(args, "--objective"),
1947
+ evidence,
1948
+ rationale,
1949
+ });
1950
+ return {
1951
+ kind,
1952
+ message: "Accepted revise_pending_wording steering.\n",
1953
+ receipt: {
1954
+ ok: true,
1955
+ kind,
1956
+ goal_id: result.goalId,
1957
+ changed_fields: result.changedFields,
1958
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1959
+ },
1960
+ };
1961
+ }
1962
+ case "annotate_ledger": {
1963
+ await annotateUltragoalLedger({ cwd, plan, evidence, rationale });
1964
+ return {
1965
+ kind,
1966
+ message: "Accepted annotate_ledger steering.\n",
1967
+ receipt: { ok: true, kind, ledger_path: getUltragoalPaths(cwd).ledgerPath },
1968
+ };
1969
+ }
1970
+ case "mark_blocked_superseded": {
1971
+ const result = await markBlockedUltragoalSuperseded({
1972
+ cwd,
1973
+ plan,
1974
+ goalId: flagValue(args, "--goal-id") ?? "",
1975
+ evidence,
1976
+ rationale,
1977
+ });
1978
+ return {
1979
+ kind,
1980
+ message: "Accepted mark_blocked_superseded steering.\n",
1981
+ receipt: {
1982
+ ok: true,
1983
+ kind,
1984
+ goal_id: result.goalId,
1985
+ no_replacement_required: true,
1986
+ goals_path: getUltragoalPaths(cwd).goalsPath,
1987
+ },
1988
+ };
1989
+ }
1990
+ }
1991
+ } catch (error) {
1992
+ const reason = error instanceof Error ? error.message : String(error);
1993
+ await appendSteeringRejected({
1994
+ cwd,
1995
+ kind,
1996
+ reason,
1997
+ goalId: flagValue(args, "--goal-id"),
1998
+ evidence,
1999
+ rationale,
2000
+ payload: steeringPayloadSummary(args),
2001
+ });
2002
+ throw error;
2003
+ }
2004
+ }
2005
+
1526
2006
  async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<UltragoalCommandResult> {
1527
2007
  const help = renderUltragoalHelp(args);
1528
2008
  if (help) return { status: 0, stdout: help };
@@ -1577,26 +2057,10 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
1577
2057
  };
1578
2058
  }
1579
2059
  case "steer": {
1580
- const kind = flagValue(args, "--kind");
1581
- if (kind !== "add_subgoal") throw new Error("native steering currently supports --kind add_subgoal");
1582
- const plan = await addUltragoalSubgoal({
1583
- cwd,
1584
- title: flagValue(args, "--title") ?? "",
1585
- objective: flagValue(args, "--objective") ?? "",
1586
- evidence: flagValue(args, "--evidence") ?? "",
1587
- rationale: flagValue(args, "--rationale") ?? "",
1588
- });
1589
- const goal = plan.goals.at(-1);
2060
+ const result = await executeUltragoalSteeringCommand(args, cwd);
1590
2061
  return {
1591
2062
  status: 0,
1592
- stdout: json
1593
- ? renderCliWriteReceipt({
1594
- ok: true,
1595
- kind,
1596
- goal_id: goal?.id,
1597
- goals_path: getUltragoalPaths(cwd).goalsPath,
1598
- })
1599
- : "Accepted add_subgoal steering.\n",
2063
+ stdout: json ? renderCliWriteReceipt(result.receipt) : result.message,
1600
2064
  };
1601
2065
  }
1602
2066
  case "record-review-blockers": {
@@ -1464,12 +1464,24 @@
1464
1464
  "name": "quality-gate-json",
1465
1465
  "type": "string"
1466
1466
  },
1467
+ {
1468
+ "appliesToVerbs": [
1469
+ "steer"
1470
+ ],
1471
+ "name": "goal-id",
1472
+ "type": "string"
1473
+ },
1467
1474
  {
1468
1475
  "appliesToVerbs": [
1469
1476
  "steer"
1470
1477
  ],
1471
1478
  "enumValues": [
1472
- "add_subgoal"
1479
+ "add_subgoal",
1480
+ "split_subgoal",
1481
+ "reorder_pending",
1482
+ "revise_pending_wording",
1483
+ "annotate_ledger",
1484
+ "mark_blocked_superseded"
1473
1485
  ],
1474
1486
  "name": "kind",
1475
1487
  "type": "enum"
@@ -1497,6 +1509,20 @@
1497
1509
  "name": "rationale",
1498
1510
  "type": "string"
1499
1511
  },
1512
+ {
1513
+ "appliesToVerbs": [
1514
+ "steer"
1515
+ ],
1516
+ "name": "replacements-json",
1517
+ "type": "string"
1518
+ },
1519
+ {
1520
+ "appliesToVerbs": [
1521
+ "steer"
1522
+ ],
1523
+ "name": "order-json",
1524
+ "type": "string"
1525
+ },
1500
1526
  {
1501
1527
  "appliesToVerbs": [
1502
1528
  "status",
@@ -267,10 +267,25 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
267
267
  },
268
268
  { name: "gjc-goal-json", type: "string", appliesToVerbs: ["checkpoint", "record-review-blockers"] },
269
269
  { name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint"] },
270
- { name: "kind", type: "enum", enumValues: ["add_subgoal"], appliesToVerbs: ["steer"] },
270
+ { name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
271
+ {
272
+ name: "kind",
273
+ type: "enum",
274
+ enumValues: [
275
+ "add_subgoal",
276
+ "split_subgoal",
277
+ "reorder_pending",
278
+ "revise_pending_wording",
279
+ "annotate_ledger",
280
+ "mark_blocked_superseded",
281
+ ],
282
+ appliesToVerbs: ["steer"],
283
+ },
271
284
  { name: "title", type: "string", appliesToVerbs: ["record-review-blockers", "steer"] },
272
285
  { name: "objective", type: "string", appliesToVerbs: ["record-review-blockers", "steer"] },
273
286
  { name: "rationale", type: "string", appliesToVerbs: ["steer"] },
287
+ { name: "replacements-json", type: "string", appliesToVerbs: ["steer"] },
288
+ { name: "order-json", type: "string", appliesToVerbs: ["steer"] },
274
289
  {
275
290
  name: "json",
276
291
  type: "boolean",