@zq-silk/yui 2.8.0 → 2.9.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 (86) hide show
  1. package/dist/cli/commandCatalog.js +35 -12
  2. package/dist/cli/interactionPolicy.js +2 -2
  3. package/dist/cli/updatePorts.js +20 -0
  4. package/dist/commands/durableJobCommands.js +50 -0
  5. package/dist/commands/projectCommands.js +4 -33
  6. package/dist/commands/scheduleCommands.js +45 -0
  7. package/dist/commands/taskActivationCommands.js +19 -5
  8. package/dist/commands/taskCommands.js +2 -1
  9. package/dist/commands/taskFactCommands.js +24 -12
  10. package/dist/commands/taskPublicationCommands.js +6 -3
  11. package/dist/commands/taskPublicationVerifyCommand.js +74 -28
  12. package/dist/context/runContextPack.js +24 -4
  13. package/dist/context/taskContext.js +52 -15
  14. package/dist/context/taskPhase.js +99 -0
  15. package/dist/controlPlaneCli.js +24 -5
  16. package/dist/controller/controller.js +35 -0
  17. package/dist/controller/fileSchedulerStoreAdapter.js +16 -1
  18. package/dist/controller/jobControl.js +42 -12
  19. package/dist/controller/jobDefinitionControl.js +304 -0
  20. package/dist/controller/jobSupervisor.js +47 -32
  21. package/dist/controller/runtime.js +11 -2
  22. package/dist/controller/scheduleControl.js +176 -0
  23. package/dist/coordination/workMailbox.js +3 -3
  24. package/dist/external/githubPublicationVerifier.js +14 -5
  25. package/dist/external/gitlabPublicationVerifier.js +2 -1
  26. package/dist/interaction/notificationPresentation.js +3 -3
  27. package/dist/interaction/operatorPresentation.js +5 -0
  28. package/dist/job/builtinJob.js +58 -0
  29. package/dist/job/durableJob.js +72 -49
  30. package/dist/job/jobDefinition.js +68 -0
  31. package/dist/job/jobRunner.js +2 -1
  32. package/dist/job/projectRefreshRunner.js +48 -0
  33. package/dist/repository/projectRefresh.js +58 -0
  34. package/dist/repository/taskWorkspacePreparer.js +2 -1
  35. package/dist/schedule/schedule.js +108 -0
  36. package/dist/scheduler/operatorEvent.js +2 -0
  37. package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
  38. package/dist/storage/contextRecords.js +8 -0
  39. package/dist/storage/jobSchema.js +15 -0
  40. package/dist/storage/maintenanceCaller.js +69 -0
  41. package/dist/storage/recordValidation.js +5 -0
  42. package/dist/storage/scheduleSchema.js +12 -0
  43. package/dist/storage/sqliteSchema.js +75 -25
  44. package/dist/storage/sqliteStore.js +110 -9
  45. package/dist/storage/storageVersions.js +1 -1
  46. package/dist/storage/storeRpc.js +5 -0
  47. package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
  48. package/dist/task/publicationToolObservation.js +128 -0
  49. package/dist/task/taskActivationService.js +35 -0
  50. package/dist/web/assets/client/app/settings.js +7 -1
  51. package/dist/web/assets/client/i18n/en.js +61 -0
  52. package/dist/web/assets/client/i18n/zh-CN.js +61 -0
  53. package/dist/web/assets/client/index.js +2 -0
  54. package/dist/web/assets/client/views/jobs.js +222 -0
  55. package/dist/web/assets/client/views/task/page.js +11 -1
  56. package/dist/web/assets/shared/manageNav.js +1 -0
  57. package/dist/web/assets/shared/taskTabs.js +2 -2
  58. package/dist/web/assets/shell/settingsContent.js +3 -0
  59. package/dist/web/assets/styles/index.js +2 -0
  60. package/dist/web/assets/styles/views/jobs.js +19 -0
  61. package/dist/web/webJobs.js +52 -0
  62. package/dist/web/webServer.js +31 -0
  63. package/docs/cli-information-contract.md +43 -2
  64. package/docs/cli-information-contract.zh-CN.md +32 -1
  65. package/docs/job-runs.md +224 -0
  66. package/docs/release-workflow.md +65 -0
  67. package/docs/release-workflow.zh-CN.md +49 -0
  68. package/docs/schedules.md +132 -0
  69. package/docs/storage-baseline.md +18 -1
  70. package/docs/storage-baseline.zh-CN.md +15 -1
  71. package/docs/task-delivery.md +68 -2
  72. package/docs/task-delivery.zh-CN.md +33 -1
  73. package/docs/testing/verification-levels.md +6 -2
  74. package/docs/testing/verification-levels.zh-CN.md +6 -3
  75. package/package.json +4 -3
  76. package/skills/yui-leader/SKILL.md +18 -1
  77. package/skills/yui-leader/VERSION +1 -1
  78. package/skills/yui-leader/references/planning.md +9 -1
  79. package/skills/yui-reviewer/SKILL.md +4 -0
  80. package/skills/yui-reviewer/VERSION +1 -1
  81. package/skills/yui-runtime/SKILL.md +9 -3
  82. package/skills/yui-runtime/VERSION +1 -1
  83. package/skills/yui-runtime/references/context-ordering.md +103 -0
  84. package/skills/yui-runtime/references/publication.md +44 -4
  85. package/skills/yui-worker/SKILL.md +5 -0
  86. package/skills/yui-worker/VERSION +1 -1
@@ -314,8 +314,8 @@ const taskChildren = [
314
314
  name: "request", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
315
315
  summary: "Record an Activation request. Every active Task gets a managed main workspace. empty adds no execution environment; scratch adopts a separate scratch environment; local requires a registered Resource and grant, not a Project ID.",
316
316
  usage: "yui task activation request <task> --request-id <id> "
317
- + "--environment <empty|scratch|local:<resource>:<read|write>>",
318
- options: ["--request-id", "--environment"],
317
+ + "--environment <empty|scratch|local:<resource>:<read|write>> [--source-message <id> | --source-text <exact-input>]",
318
+ options: ["--request-id", "--environment", "--source-message", "--source-text"],
319
319
  examples: [
320
320
  "yui task activation request <task> --request-id start-1 --environment empty",
321
321
  "yui task activation request <task> --request-id start-1 --environment scratch"
@@ -643,8 +643,8 @@ const taskChildren = [
643
643
  {
644
644
  name: "upsert", discovery: { surface: "managed", audiences: ["operator", "leader"] },
645
645
  summary: "Create or immutably update an external PR/MR and its publication state.",
646
- usage: "yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>]",
647
- options: ["--project", "--provider", "--repository", "--kind", "--id", "--url", "--title", "--source-branch", "--target-branch", "--local-commit", "--head-commit", "--remote-commit", "--state", "--reported", "--verified", "--evidence", "--merged-at"]
646
+ usage: "yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported] [--evidence <text>] [--merged-at <iso-timestamp>]",
647
+ options: ["--project", "--provider", "--repository", "--kind", "--id", "--url", "--title", "--source-branch", "--target-branch", "--local-commit", "--head-commit", "--remote-commit", "--state", "--reported", "--evidence", "--merged-at"]
648
648
  },
649
649
  {
650
650
  name: "diff", discovery: { surface: "managed", audiences: ["operator", "leader"] },
@@ -660,8 +660,9 @@ const taskChildren = [
660
660
  },
661
661
  {
662
662
  name: "verify", discovery: { surface: "managed", audiences: ["operator", "leader"] },
663
- summary: "Observe the current PR/MR head and merge through its provider; record verification separately from Task coverage.",
664
- usage: "yui task publication verify (<task>/<publication-id> | <task> <publication-id>)"
663
+ summary: "Record exact authorized tool evidence, or query the optional gh/glab adapter; verification is separate from Task coverage.",
664
+ usage: "yui task publication verify (<task>/<publication-id> | <task> <publication-id>) [--observation <json>]",
665
+ options: ["--observation"]
665
666
  },
666
667
  {
667
668
  name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
@@ -1016,15 +1017,15 @@ const taskChildren = [
1016
1017
  },
1017
1018
  {
1018
1019
  name: "brief", discovery: { surface: "managed", audiences: ["operator", "leader"] },
1019
- summary: "Manage the Task Brief, the authoritative summary of current task state.",
1020
+ summary: "Manage the Leader-maintained Task summary; live lifecycle and authority remain separate facts.",
1020
1021
  sections: [{ id: "manage", title: "Commands", entries: ["show", "update"] }],
1021
1022
  children: [
1022
1023
  { name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show the Task Brief.", usage: "yui task brief show <task> [--cursor <cursor>]", options: ["--cursor"] },
1023
1024
  {
1024
1025
  name: "update", discovery: { surface: "managed", audiences: ["operator", "leader"] },
1025
1026
  summary: "Create or update the Task Brief.",
1026
- usage: "yui task brief update <task> [--objective <text>] [--boundary <text> ...] [--approach <text>] [--focus <text>] [--leader-summary <text>]",
1027
- options: ["--objective", "--boundary", "--approach", "--focus", "--leader-summary"]
1027
+ usage: "yui task brief update <task> --if-current <writeGuard> [--objective <text>] [--boundary <text> ...] [--approach <text>] [--focus <text>] [--leader-summary <text>]",
1028
+ options: ["--if-current", "--objective", "--boundary", "--approach", "--focus", "--leader-summary"]
1028
1029
  }
1029
1030
  ]
1030
1031
  },
@@ -1136,7 +1137,7 @@ export const ROOT_COMMAND = buildNode({
1136
1137
  ] },
1137
1138
  { id: "workflow", title: "Workflow", entries: ["agent", "operator", "role", "inbox", "project", "task"] },
1138
1139
  { id: "configuration", title: "Configuration", entries: ["config"] },
1139
- { id: "operations", title: "Operations", entries: ["web", "controller", "session", "execution", "capability", "job", "jobs", "telemetry", "release"] },
1140
+ { id: "operations", title: "Operations", entries: ["web", "controller", "session", "execution", "capability", "job", "schedule", "jobs", "telemetry", "release"] },
1140
1141
  { id: "resources", title: "Resources", entries: ["resources"] },
1141
1142
  { id: "internal", title: "Internal", entries: ["internal"] }
1142
1143
  ],
@@ -1641,15 +1642,37 @@ export const ROOT_COMMAND = buildNode({
1641
1642
  },
1642
1643
  {
1643
1644
  name: "job", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1644
- summary: "Start, inspect, cancel, or acknowledge a Controller-managed DurableJob.",
1645
- sections: [{ id: "manage", title: "Commands", entries: ["start", "get", "cancel", "acknowledge"] }],
1645
+ summary: "Manage stable Job definitions and exact execution Runs (not scheduler wakes).",
1646
+ sections: [{ id: "manage", title: "Commands", entries: ["define", "update", "list", "show", "run", "runs", "run-get", "logs", "run-cancel", "run-acknowledge", "start", "get", "cancel", "acknowledge"] }],
1646
1647
  children: [
1648
+ ...["define", "update", "list", "show", "run", "runs", "run-get", "logs", "run-cancel", "run-acknowledge"].map(name => ({
1649
+ name, discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1650
+ summary: `${name} a stable Job or exact Run.`,
1651
+ usage: `yui job ${name} (--task <id>|--global)${name === "list" ? "" : " --job <id>"}`
1652
+ + (name === "define" || name === "update" ? " --title <text> --input <configuration-json>" : "")
1653
+ + (name === "update" ? " --revision <current>" : "")
1654
+ + (name === "run" ? " --request-id <key> [--retry-of <run-id>]" : "")
1655
+ + (["run-get", "logs", "run-cancel", "run-acknowledge"].includes(name) ? " --run <run-id>" : "")
1656
+ })),
1647
1657
  { name: "start", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Start a DurableJob under an explicit request identity.", usage: "yui job start --request-id <id> --task <id> --project <project> --head <sha> --workspace <dir> --step <name>=<command> [--step ...] [--owner task|work-item:<id>|integration-attempt:<id>] [--env <k=v>...]" },
1648
1658
  { name: "get", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Show a DurableJob record and its terminal result.", usage: "yui job get --task <id> --job <job-id>" },
1649
1659
  { name: "cancel", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Request cancellation of a running or queued DurableJob.", usage: "yui job cancel --task <id> --job <job-id>" },
1650
1660
  { name: "acknowledge", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Acknowledge an unknown-needs-attention DurableJob so Task lifecycle gates can proceed.", usage: "yui job acknowledge --task <id> --job <job-id>" }
1651
1661
  ]
1652
1662
  },
1663
+ {
1664
+ name: "schedule", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1665
+ summary: "Manage persistent time plans referencing scoped Jobs.",
1666
+ sections: [{ id: "manage", title: "Commands", entries: ["create", "list", "show", "update", "enable", "disable"] }],
1667
+ children: ["create", "list", "show", "update", "enable", "disable"].map(name => ({
1668
+ name, discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1669
+ summary: `${name} a Schedule; disabling never cancels an admitted Run.`,
1670
+ usage: `yui schedule ${name} (--global|--task <id>)${name === "list" ? "" : " --schedule <id>"}`
1671
+ + (name === "create" ? " --job <id> --rule <json>" : "")
1672
+ + (name === "update" ? " --rule <json> [--job <id>]" : "")
1673
+ + (["update", "enable", "disable"].includes(name) ? " --revision <observed>" : "")
1674
+ }))
1675
+ },
1653
1676
  {
1654
1677
  name: "jobs", discovery: { surface: "public", audiences: ["public", "operator"] },
1655
1678
  summary: "Inspect scheduler wake and recovery records.",
@@ -185,7 +185,7 @@ export const INTERACTION_POLICIES = Object.freeze([
185
185
  actionTarget: true,
186
186
  statuses: ["draft"]
187
187
  }],
188
- trailingOptions: { "--request-id": "value", "--environment": "value" }
188
+ trailingOptions: { "--request-id": "value", "--environment": "value", "--source-message": "value", "--source-text": "value" }
189
189
  },
190
190
  {
191
191
  commandPath: ["task", "activation", "cancel"],
@@ -615,7 +615,7 @@ export const INTERACTION_POLICIES = Object.freeze([
615
615
  ].map(([group, command]) => {
616
616
  const trailingOptions = {};
617
617
  if (group === "brief" && command === "update") {
618
- Object.assign(trailingOptions, { "--objective": "value", "--boundary": "value", "--approach": "value", "--focus": "value", "--leader-summary": "value" });
618
+ Object.assign(trailingOptions, { "--if-current": "value", "--objective": "value", "--boundary": "value", "--approach": "value", "--focus": "value", "--leader-summary": "value" });
619
619
  }
620
620
  else if (group === "decision" && command === "record") {
621
621
  Object.assign(trailingOptions, { "--title": "value", "--rationale": "value" });
@@ -776,6 +776,26 @@ function interpretPreflight(result) {
776
776
  // error is not a safe preflight — block rather than proceed to a switch.
777
777
  const data = parseSuccessEnvelopeData(result);
778
778
  if (data === null) {
779
+ // CLI bootstrap failures use an ok:false envelope on stderr. Preserve its
780
+ // already-sanitized diagnosis (including recovery guidance), never promote
781
+ // an error envelope to a successful storage preflight.
782
+ for (const stream of [result.stderr, result.stdout]) {
783
+ let failure;
784
+ try {
785
+ failure = JSON.parse(stream.toString("utf8").trim());
786
+ }
787
+ catch {
788
+ continue;
789
+ }
790
+ if (!isRecord(failure) || failure.ok !== false || typeof failure.message !== "string")
791
+ continue;
792
+ return {
793
+ status: "blocked",
794
+ stage: "preflight",
795
+ message: `Staged binary preflight failed (exit ${result.status ?? "null"}): ${failure.message}`,
796
+ action: "Resolve the reported staged-binary failure using the same Home and matching CLI; do not force the update."
797
+ };
798
+ }
779
799
  return {
780
800
  status: "blocked",
781
801
  stage: "preflight",
@@ -1,5 +1,6 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
2
  import { ensureFileTaskController } from "../controller/clientRuntime.js";
3
+ import { callFileTaskController } from "../controller/clientRuntime.js";
3
4
  import { acknowledgeDurableJob, cancelDurableJob, getDurableJob, startDurableJob } from "../controller/jobClient.js";
4
5
  import { resolveJobCaller, taskLocalActor } from "../task/taskAuthority.js";
5
6
  /**
@@ -14,6 +15,9 @@ export const JOB_OWNER_USAGE = "task|work-item:<id>|integration-attempt:<id>";
14
15
  */
15
16
  export async function runDurableJobCommand(args, options) {
16
17
  const [command, ...rest] = args;
18
+ if (command !== undefined && ["define", "update", "list", "show", "run", "runs", "run-get", "run-cancel", "run-acknowledge", "logs"].includes(command)) {
19
+ return definitionCommand(command, rest, options);
20
+ }
17
21
  if (command === "start")
18
22
  return startJob(rest, options);
19
23
  if (command === "get")
@@ -31,6 +35,52 @@ export async function runDurableJobCommand(args, options) {
31
35
  + "yui job cancel --task <id> --job <job-id>\n"
32
36
  + "yui job acknowledge --task <id> --job <job-id>");
33
37
  }
38
+ async function definitionCommand(command, args, options) {
39
+ const flags = new Map();
40
+ for (let i = 0; i < args.length; i++) {
41
+ const flag = args[i];
42
+ if (!["--global", "--task", "--job", "--title", "--input", "--revision", "--request-id", "--retry-of", "--run"].includes(flag)
43
+ || flags.has(flag))
44
+ throw usageError(`Unknown or repeated Job flag: ${flag}.`);
45
+ if (flag === "--global") {
46
+ flags.set(flag, "true");
47
+ continue;
48
+ }
49
+ const value = args[++i];
50
+ if (value === undefined || value.startsWith("--"))
51
+ throw usageError(`Missing value: ${flag}.`);
52
+ flags.set(flag, value);
53
+ }
54
+ if (flags.has("--global") === flags.has("--task"))
55
+ throw usageError("Choose exactly one of --task <id> or --global.");
56
+ const taskId = flags.get("--task");
57
+ const caller = resolveJobCaller(options.environment, taskId ?? "");
58
+ const scope = taskId === undefined ? { kind: "global" } : { kind: "task", taskId };
59
+ let configuration;
60
+ if (flags.has("--input")) {
61
+ try {
62
+ configuration = JSON.parse(flags.get("--input"));
63
+ }
64
+ catch {
65
+ throw usageError("--input must be a JSON Job configuration.");
66
+ }
67
+ }
68
+ const requestId = flags.get("--request-id");
69
+ if (command === "run" && !requestId)
70
+ throw usageError("job run requires --request-id.");
71
+ const params = {
72
+ op: command, scope, caller,
73
+ ...(flags.has("--job") ? { id: flags.get("--job") } : {}),
74
+ ...(flags.has("--title") ? { title: flags.get("--title") } : {}),
75
+ ...(configuration === undefined ? {} : { configuration }),
76
+ ...(flags.has("--revision") ? { expectedRevision: Number(flags.get("--revision")) } : {}),
77
+ ...(flags.has("--run") ? { runId: flags.get("--run") } : {}),
78
+ ...(flags.has("--retry-of") ? { retryOf: flags.get("--retry-of") } : {}),
79
+ ...(command === "run" ? { trigger: { source: flags.has("--retry-of") ? "retry" : "manual", key: requestId } } : {})
80
+ };
81
+ const result = await callFileTaskController(options.home, "job.definition", params, { environment: options.environment });
82
+ return `${JSON.stringify(result, null, 2)}\n`;
83
+ }
34
84
  async function startJob(args, options) {
35
85
  const parsed = parseStartArgs(args);
36
86
  await ensureFileTaskController(options.home, { environment: options.environment });
@@ -1,12 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readdir, rename, rm } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { CliError, usageError } from "../errors/cliError.js";
4
+ import { usageError } from "../errors/cliError.js";
5
5
  import { defaultTableWidth, renderTable } from "../output/table.js";
6
6
  import { boundedDocument, readOptions, recordPage } from "../output/boundedRead.js";
7
7
  import { healCheckoutSwap, restoreCheckoutSwap, swapManagedCheckout } from "../repository/checkoutSwap.js";
8
- import { GitWorkspaceRefreshError, NodeGitWorkspace } from "../repository/gitWorkspace.js";
8
+ import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
9
9
  import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
10
+ import { refreshRegisteredProject } from "../repository/projectRefresh.js";
10
11
  import { addProjectKnowledge, addKnowledgeProposal, assertProjectActive, decideKnowledgeProposal, createProject, findKnowledgeProposal, findKnowledgeProposalByFingerprint, knowledgeEvidenceDigest, knowledgeProposalFingerprint, managedProjectPath, planKnowledgeAcceptance, retireProject, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
11
12
  import { managedWorkspacesRoot } from "../storage/homeLayout.js";
12
13
  import { projectActor } from "../task/taskAuthority.js";
@@ -90,37 +91,7 @@ async function refreshProject(args, store, options) {
90
91
  throw usageError("Project refresh usage: yui project refresh <project>.");
91
92
  }
92
93
  const project = requireProject(store, args[0]);
93
- assertProjectActive(project, "refresh");
94
- if (project.remoteUrl === undefined) {
95
- throw usageError(`Project refresh requires a remote URL: ${project.id}.`);
96
- }
97
- if (project.stableBranch !== project.developmentBranch) {
98
- throw usageError(`Project refresh requires matching stable and development branches: ${project.id}.`);
99
- }
100
- // RFC Phase 1: hold the per-Project maintenance fence for the whole refresh
101
- // so Task workspace preparation and other maintenance cannot interleave
102
- // with the canonical branch/working-tree move.
103
- const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
104
- try {
105
- const current = requireUnchangedMaintenanceProject(store, project, "refresh");
106
- const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
107
- repositoryPath: current.path,
108
- remoteUrl: current.remoteUrl,
109
- stableRef: current.stableBranch
110
- });
111
- return { project: current, ...refreshed };
112
- }
113
- catch (error) {
114
- if (error instanceof GitWorkspaceRefreshError) {
115
- throw new CliError("RUNTIME_ERROR", error.message, undefined, {
116
- projectId: project.id, refresh: error.result
117
- });
118
- }
119
- throw error;
120
- }
121
- finally {
122
- releaseMaintenance();
123
- }
94
+ return refreshRegisteredProject(store, project.id, options);
124
95
  }
125
96
  /**
126
97
  * RFC Phase 1: diagnose a managed Project's divergence without mutating it.
@@ -0,0 +1,45 @@
1
+ import { usageError } from "../errors/cliError.js";
2
+ import { callFileTaskController } from "../controller/clientRuntime.js";
3
+ import { resolveJobCaller } from "../task/taskAuthority.js";
4
+ export async function runScheduleCommand(args, options) {
5
+ const [op, ...rest] = args;
6
+ if (op === undefined || !["create", "list", "show", "update", "enable", "disable"].includes(op)) {
7
+ throw usageError("Schedule command must be create, list, show, update, enable, or disable.");
8
+ }
9
+ const flags = new Map();
10
+ for (let i = 0; i < rest.length; i++) {
11
+ const flag = rest[i];
12
+ if (!["--global", "--task", "--schedule", "--job", "--rule", "--revision"].includes(flag) || flags.has(flag)) {
13
+ throw usageError(`Unknown or repeated Schedule flag: ${flag}.`);
14
+ }
15
+ if (flag === "--global") {
16
+ flags.set(flag, "true");
17
+ continue;
18
+ }
19
+ const value = rest[++i];
20
+ if (value === undefined || value.startsWith("--"))
21
+ throw usageError(`Missing value: ${flag}.`);
22
+ flags.set(flag, value);
23
+ }
24
+ if (flags.has("--global") === flags.has("--task"))
25
+ throw usageError("Choose exactly one of --global or --task.");
26
+ const taskId = flags.get("--task");
27
+ let rule;
28
+ if (flags.has("--rule")) {
29
+ try {
30
+ rule = JSON.parse(flags.get("--rule"));
31
+ }
32
+ catch {
33
+ throw usageError("--rule must be JSON.");
34
+ }
35
+ }
36
+ const result = await callFileTaskController(options.home, "schedule.control", {
37
+ op, scope: taskId === undefined ? { kind: "global" } : { kind: "task", taskId },
38
+ caller: resolveJobCaller(options.environment, taskId ?? ""),
39
+ ...(flags.has("--schedule") ? { id: flags.get("--schedule") } : {}),
40
+ ...(flags.has("--job") ? { jobId: flags.get("--job") } : {}),
41
+ ...(rule === undefined ? {} : { rule }),
42
+ ...(flags.has("--revision") ? { expectedRevision: Number(flags.get("--revision")) } : {})
43
+ }, { environment: options.environment });
44
+ return result;
45
+ }
@@ -6,6 +6,7 @@ import { currentManagedRuntime } from "../runtime/managedCaller.js";
6
6
  import { describeEnvironmentPlan } from "../task/taskActivation.js";
7
7
  import { cancelTaskActivation, requestTaskActivation, taskActivationOperationRef } from "../task/taskActivationService.js";
8
8
  import { taskLocalActor } from "../task/taskAuthority.js";
9
+ import { taskPhase } from "../context/taskPhase.js";
9
10
  /**
10
11
  * Explicit Activation requests.
11
12
  *
@@ -20,7 +21,7 @@ export function runTaskActivationCommand(args, store, options) {
20
21
  switch (command) {
21
22
  case "request": return requestActivation(rest, store, options);
22
23
  case "cancel": return cancelActivation(rest, store, options);
23
- case "show": return showActivation(rest, store);
24
+ case "show": return store.readTransaction((reader) => showActivation(rest, reader));
24
25
  default:
25
26
  throw usageError(command === undefined
26
27
  ? "Task activation command is required."
@@ -28,7 +29,8 @@ export function runTaskActivationCommand(args, store, options) {
28
29
  }
29
30
  }
30
31
  const REQUEST_USAGE = "Task activation request usage: yui task activation request <task> "
31
- + "--request-id <id> --environment <empty|scratch|local:<resource>:<read|write>>.";
32
+ + "--request-id <id> --environment <empty|scratch|local:<resource>:<read|write>> "
33
+ + "[--source-message <message-id> | --source-text <exact-input>].";
32
34
  /**
33
35
  * Records the request and returns its reference immediately.
34
36
  *
@@ -39,19 +41,26 @@ const REQUEST_USAGE = "Task activation request usage: yui task activation reques
39
41
  * current then.
40
42
  */
41
43
  function requestActivation(args, store, options) {
42
- const parsed = parseTail(args, new Set(["--request-id", "--environment"]), REQUEST_USAGE);
44
+ const parsed = parseTail(args, new Set(["--request-id", "--environment", "--source-message", "--source-text"]), REQUEST_USAGE);
43
45
  exactPositionals(parsed.positionals, 1, REQUEST_USAGE);
44
46
  const taskId = parsed.positionals[0];
45
47
  const requestId = requiredOption(parsed.options, "--request-id", REQUEST_USAGE);
46
48
  const environmentPlan = parseEnvironmentPlan(requiredOption(parsed.options, "--environment", REQUEST_USAGE), REQUEST_USAGE);
47
49
  const now = options.now?.() ?? new Date();
48
50
  const caller = activationCaller(store, options, taskId);
51
+ const sourceMessageId = parsed.options.get("--source-message");
52
+ const sourceText = parsed.options.get("--source-text");
53
+ if (caller.actorId.startsWith("task:") && sourceMessageId === undefined && sourceText === undefined) {
54
+ throw usageError("Leader activation requires --source-message or --source-text with the exact new user input. Read the original; do not substitute a Brief summary.", REQUEST_USAGE);
55
+ }
49
56
  const result = attempt(REQUEST_USAGE, () => requestTaskActivation(store, {
50
57
  taskId,
51
58
  requestId,
52
59
  actorId: caller.actorId,
53
60
  authorityRef: caller.authorityRef,
54
61
  environmentPlan,
62
+ sourceMessageId, sourceText,
63
+ sourceNativeSessionId: caller.nativeSessionId,
55
64
  ...(caller.planningRunId === undefined ? {} : { callerRunId: caller.planningRunId })
56
65
  }, now));
57
66
  // An immediate request has nothing left to wait for, so ask the Controller to
@@ -64,12 +73,14 @@ function requestActivation(args, store, options) {
64
73
  void options.runtime?.notifyMailboxChanged({ kind: "task", taskId });
65
74
  }
66
75
  const request = result.request;
76
+ const source = taskPhase(store, { ...requireTask(store, taskId), activationRequest: request }).activation?.source;
67
77
  const lines = [
68
78
  `${result.created ? "Requested" : "Existing"} activation ${result.operationRef}`,
69
79
  `Task: ${taskId} (draft)`,
70
80
  `Start: ${result.startMode}${result.afterPlanningRun === undefined ? "" : ` after ${taskId}/${result.afterPlanningRun}`}`,
71
81
  `Environment: ${describeEnvironmentPlan(request.environmentPlan)}`,
72
82
  `Disposition: ${request.disposition}`,
83
+ ...(source?.read === undefined ? [] : [`Source: yui ${source.read}`]),
73
84
  result.startMode === "after-planning-turn"
74
85
  ? "This reference is returned now; the request is adopted after the planning Turn ends."
75
86
  : `Adopt it with: yui task activate ${taskId}`
@@ -81,7 +92,8 @@ function requestActivation(args, store, options) {
81
92
  ...(result.afterPlanningRun === undefined
82
93
  ? {}
83
94
  : { afterPlanningRun: result.afterPlanningRun }),
84
- request
95
+ request,
96
+ source
85
97
  });
86
98
  }
87
99
  /** A cancelled request is never adopted, even if the same id is retried. */
@@ -139,7 +151,8 @@ function showActivation(args, store) {
139
151
  `Requested at: ${request.requestedAt}`,
140
152
  ...settledLines
141
153
  ];
142
- return output(`${lines.join("\n")}\n`, { task, request, settledRequests: settled });
154
+ return output(`${lines.join("\n")}\n`, { task, request, settledRequests: settled,
155
+ phase: taskPhase(store, task) });
143
156
  }
144
157
  /**
145
158
  * Resolves who is requesting, and whether they are running inside this Task's
@@ -161,6 +174,7 @@ function activationCaller(store, options, taskId) {
161
174
  : store.getRun(taskId, runtime.currentRunId);
162
175
  return {
163
176
  actorId: `task:${taskId}/role:${SYSTEM_LEADER_ROLE}`,
177
+ nativeSessionId: runtime.nativeSessionId,
164
178
  // Non-secret binding fingerprint of the live Session, matching the Job
165
179
  // boundary: reattaching the same Session keeps it, replacing it does not.
166
180
  authorityRef: fingerprint([
@@ -665,6 +665,7 @@ function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey,
665
665
  requestId: `submit-${message.id}`,
666
666
  actorId: identity.actorId,
667
667
  authorityRef: identity.authorityRef,
668
+ sourceMessageId: message.id,
668
669
  environmentPlan: routing.environmentPlan
669
670
  }, now);
670
671
  activationRef = taskActivationOperationRef(task.id, `submit-${message.id}`);
@@ -7008,7 +7009,7 @@ function taskWakeInspectionCommand(args, store) {
7008
7009
  const windowEvents = allEvents.filter((e) => inWindow(e.createdAt) || referenced("event", e.id) || reasonEvent(e));
7009
7010
  const events = history ? windowEvents : windowEvents.filter(e => referenced("event", e.id)
7010
7011
  || reasonEvent(e) || ["run.completed", "run.failed", "run.cancelled"].includes(e.type));
7011
- const messages = wakeMessages(wake, store.listMessages(task.id));
7012
+ const messages = wakeMessages(wake, store.listMessages(task.id), history);
7012
7013
  const allRuns = store.listRuns(task.id);
7013
7014
  const referencedRunIds = new Set(referencedWakeRunIds(allRuns, allEvents, events));
7014
7015
  for (const run of allRuns)
@@ -1,5 +1,5 @@
1
1
  import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
2
- import { assertContextRecordReadable } from "../context/taskContext.js";
2
+ import { assertContextRecordReadable, resolveContextReader } from "../context/taskContext.js";
3
3
  import { taskRecordList } from "./taskContextCommand.js";
4
4
  import { materialize } from "../context/taskContext.js";
5
5
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
@@ -8,15 +8,22 @@ import { dataError, usageError } from "../errors/cliError.js";
8
8
  import { createMilestone } from "../milestone/milestone.js";
9
9
  import { assertTaskOpen, clock, exactPositionals, leaderActionEventPayload, leaderMailbox, notifyMailbox, output, parseMultiValueTail, parseTail, presentTime, recordTaskEvent, requiredOption, requiredText, requireTask, taskActor, taskMailbox, taskRef } from "./taskCommandSupport.js";
10
10
  import { failureCapabilitiesCommand } from "../runtime/agentFailureContext.js";
11
+ import { taskBriefGuard, taskPhase } from "../context/taskPhase.js";
11
12
  export function taskBriefCommand(args, store, options) {
12
13
  const [command, ...rest] = args;
13
14
  if (command === "show") {
14
15
  exactPositionals(rest, 1, "Task brief show usage: yui task brief show <task>.");
15
- const task = requireTask(store, rest[0]);
16
- assertContextRecordReadable(store, task.id, "task-brief", task.id, options.environment);
17
- const brief = store.getTaskBrief(task.id);
16
+ const { task, brief, writeGuard, phase } = store.readTransaction(reader => {
17
+ const task = requireTask(reader, rest[0]);
18
+ assertContextRecordReadable(reader, task.id, "task-brief", task.id, options.environment);
19
+ const caller = resolveContextReader(reader, options.environment ?? {});
20
+ const shared = caller === undefined || caller.roleName === "leader";
21
+ return { task, brief: reader.getTaskBrief(task.id),
22
+ writeGuard: shared ? taskBriefGuard(reader, task) : undefined,
23
+ phase: shared ? taskPhase(reader, task) : undefined };
24
+ });
18
25
  if (brief === null) {
19
- return output(`Task ${task.id} has no brief.\n`, { taskId: task.id, brief: null });
26
+ return output(`Task ${task.id} has no brief.\n${writeGuard === undefined ? "" : `Write guard: ${writeGuard}\n`}`, { taskId: task.id, brief: null, writeGuard, phase });
20
27
  }
21
28
  const timeZone = store.getConfig().timeZone;
22
29
  const working = task.status === "draft" || task.status === "active";
@@ -32,12 +39,13 @@ export function taskBriefCommand(args, store, options) {
32
39
  `${working ? "Current focus" : "Last working focus"}: ${brief.currentFocus}`,
33
40
  `${working ? "Leader summary" : "Last working summary"}: ${brief.leaderSummary}`,
34
41
  `Updated by: ${brief.updatedBy}`,
35
- `Updated at: ${presentTime(brief.updatedAt, timeZone)}`
36
- ].join("\n").concat("\n"), { taskId: task.id, brief });
42
+ `Updated at: ${presentTime(brief.updatedAt, timeZone)}`,
43
+ ...(writeGuard === undefined ? [] : [`Write guard: ${writeGuard}`])
44
+ ].join("\n").concat("\n"), { taskId: task.id, brief, writeGuard, phase });
37
45
  }
38
46
  if (command === "update") {
39
- const usage = "Task brief update usage: yui task brief update <task> [--objective <text>] [--boundary <text> ...] [--approach <text>] [--focus <text>] [--leader-summary <text>].";
40
- const parsed = parseMultiValueTail(rest, new Set(["--objective", "--approach", "--focus", "--leader-summary"]), new Set(["--boundary"]), usage);
47
+ const usage = "Task brief update usage: yui task brief update <task> --if-current <writeGuard> [--objective <text>] [--boundary <text> ...] [--approach <text>] [--focus <text>] [--leader-summary <text>].";
48
+ const parsed = parseMultiValueTail(rest, new Set(["--objective", "--approach", "--focus", "--leader-summary", "--if-current"]), new Set(["--boundary"]), usage);
41
49
  exactPositionals(parsed.positionals, 1, usage);
42
50
  const hasObjective = parsed.options.has("--objective");
43
51
  const hasApproach = parsed.options.has("--approach");
@@ -53,6 +61,9 @@ export function taskBriefCommand(args, store, options) {
53
61
  assertTaskOpen(task);
54
62
  const existing = tx.getTaskBrief(task.id);
55
63
  const updatedBy = taskActor(tx, options, task.id);
64
+ if (parsed.options.get("--if-current") !== taskBriefGuard(tx, task)) {
65
+ throw usageError(`Task context or Brief changed, or --if-current is missing; read task brief show ${task.id}, reconcile new input, then retry with its writeGuard.`);
66
+ }
56
67
  const brief = existing === null
57
68
  ? createTaskBrief({
58
69
  objective: requiredText(parsed.options.get("--objective"), "--objective"),
@@ -244,9 +255,10 @@ export function taskEventCommand(args, store, options = {}) {
244
255
  exactPositionals(rest, 2, "Task event show usage: yui task event show <task> <event>.");
245
256
  const task = requireTask(store, rest[0]);
246
257
  assertContextRecordReadable(store, task.id, "task-event", rest[1], options.environment);
247
- const events = store.listEvents(task.id);
248
- const event = events.find((e) => e.id === rest[1]) ?? null;
249
- if (event === null)
258
+ const event = store.queryContextRecords(task.id, {
259
+ family: "task-event", ids: [rest[1]], limit: 1
260
+ }).records[0]?.value;
261
+ if (event === undefined)
250
262
  throw dataError(`Event not found: ${rest[1]}.`);
251
263
  const timeZone = store.getConfig().timeZone;
252
264
  const capabilitiesCommand = event.type === "runtime.agent-error" && event.payload.roleName
@@ -35,7 +35,7 @@ export function runPublicationCommand(args, store, options) {
35
35
  }
36
36
  }
37
37
  function upsertPublication(args, store, options) {
38
- const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>].";
38
+ const usage = "Task publication upsert usage: yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported] [--evidence <text>] [--merged-at <iso-timestamp>].";
39
39
  const parsed = parseUpsertArgs(args, usage);
40
40
  exactPositionals(parsed.positionals, 1, usage);
41
41
  const task = requireTask(store, parsed.positionals[0]);
@@ -173,8 +173,9 @@ function parseVerification(parsed, usage) {
173
173
  if (parsed.flags.has("--reported") && parsed.flags.has("--verified")) {
174
174
  throw usageError("--reported and --verified are mutually exclusive.", usage);
175
175
  }
176
- if (parsed.flags.has("--verified"))
177
- return "verified";
176
+ if (parsed.flags.has("--verified")) {
177
+ throw usageError("Use task publication verify with a provider read or --observation tool evidence; upsert cannot assert --verified.");
178
+ }
178
179
  if (parsed.flags.has("--reported"))
179
180
  return "reported";
180
181
  return undefined;
@@ -210,6 +211,8 @@ function inheritPublicationInput(existing, input) {
210
211
  && normalizeCommitForComparison(input.headCommit) !== existing.headCommit)
211
212
  || input.projectId !== existing.projectId
212
213
  || input.externalKind !== existing.externalKind
214
+ || (input.externalUrl !== undefined && input.externalUrl !== existing.externalUrl)
215
+ || (input.targetBranch !== undefined && input.targetBranch !== existing.targetBranch)
213
216
  || stateChanged
214
217
  || remoteCommitChanged
215
218
  || evidenceChanged