toolroll 0.9.84 → 0.9.86
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/CHANGELOG.md +66 -0
- package/README.md +94 -1193
- package/dist/agent-fence.d.ts +9 -5
- package/dist/agent-fence.js +6 -5
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +33 -31
- package/dist/browser-workspace.d.ts +45 -0
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +36 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +6 -4
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/console-api.d.ts +6 -2
- package/dist/contracts/console-api.js +6 -2
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +51 -0
- package/dist/contracts/lead-tools.js +5 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/coordinator.js +1 -0
- package/dist/deploy-gate.js +4 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +47 -2
- package/dist/flows-ui.js +11 -2
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +65 -10
- package/dist/lead-voice.js +16 -5
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/operate.d.ts +2 -3
- package/dist/operate.js +226 -21
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/pull-request-flow.js +35 -4
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-follow-ups.js +4 -0
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +7 -0
- package/dist/server/chat.js +4 -2
- package/dist/server/console.css +41 -0
- package/dist/server/flows.js +15 -0
- package/dist/server/pages.js +11 -1
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +15 -2
- package/dist/server/render-pages.js +52 -4
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-tasks.d.ts +9 -0
- package/dist/server/render-tasks.js +32 -11
- package/dist/server/route-table.js +12 -0
- package/dist/server/settings.js +18 -1
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +95 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/store.d.ts +116 -5
- package/dist/store.js +385 -24
- package/dist/surface.js +18 -2
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +28 -0
- package/dist/work-index.js +82 -28
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +3 -2
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
package/dist/operate.js
CHANGED
|
@@ -28,6 +28,8 @@ import { idleStorageSweep, sweepSchedule, sweepScheduleWords } from "./storage-s
|
|
|
28
28
|
import { lastSweep, saveSweep, sweepDetails, sweepWords as storageSweepWords } from "./storage-sweep.js";
|
|
29
29
|
import { removeShared, sharedDepsRoot, sharedUse, unusedShared } from "./shared-deps.js";
|
|
30
30
|
import { cleanLandedBacklog, gitOf, landedBacklog, reconcileLanded, BACKLOG_DAYS } from "./landed-cleanup.js";
|
|
31
|
+
import { accessOf, applyTidy, approverStandsNow, withinLens, familyRepoOf, releaseFactsFor, runTidy, tidyDays, tidyDigest, tidyGroups, tidyPreview, TIDY_DEFAULT_DAYS, TIDY_PASS_LIMIT } from "./tidy.js";
|
|
32
|
+
import { personProof } from "./person-proof.js";
|
|
31
33
|
import { RemoteMains } from "./remote-main.js";
|
|
32
34
|
import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
|
|
33
35
|
import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
|
|
@@ -54,6 +56,7 @@ import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
|
|
|
54
56
|
import { origin, readMonitoring } from "./monitoring-settings.js";
|
|
55
57
|
import { logEvent } from "./log.js";
|
|
56
58
|
import { FLOW_EVERY_MS, flowHousekeeping, moveCards, moveCardsAfter } from "./flow-cadence.js";
|
|
59
|
+
import { NO_YOUIYOUX_PASS, youiyouxOnboardLine } from "./youiyoux-intake.js";
|
|
57
60
|
import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
|
|
58
61
|
import { sendSubagentSummaries } from "./subagent-admin.js";
|
|
59
62
|
import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
|
|
@@ -104,7 +107,7 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
104
107
|
import { homedir, hostname } from "node:os";
|
|
105
108
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
106
109
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
107
|
-
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
110
|
+
import { openStore, openStoreNoMigrate, archiveTaskFamily, restoreTaskFamily, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
108
111
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
109
112
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
110
113
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
@@ -227,7 +230,7 @@ import { isTaskSize, legOf, projectRoute, routeDigestOf, routeWords, TASK_SIZES,
|
|
|
227
230
|
import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
|
|
228
231
|
import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
|
|
229
232
|
import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
|
|
230
|
-
import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, saveConsoleUrl, savePrimary } from "./webhooks.js";
|
|
233
|
+
import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, readUpConsoleUrl, saveConsoleUrl, savePrimary, UP_CONSOLE_FILE } from "./webhooks.js";
|
|
231
234
|
import { auditOf, inspectionOf, isProviderId, MONEY_CAPABILITIES, PROVIDER_IDS, validModelId, validateSpec, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
232
235
|
import { attestProvider, attestationOf, versionInRange } from "./attest.js";
|
|
233
236
|
import { build, proveApprovedProfile, } from "./builder.js";
|
|
@@ -272,12 +275,17 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
272
275
|
the lead passes it as --token, or TOOLROLL_LEAD_TOKEN, and its work pings nobody
|
|
273
276
|
toolroll task ask <id> --person <name> --why "<text>"
|
|
274
277
|
hand a task to a person: it pings them, and it is theirs
|
|
275
|
-
toolroll task list [--view <v>] [--limit <n>] [--cursor <c>] paginated saved task status
|
|
278
|
+
toolroll task list [--view <v>] [--limit <n>] [--cursor <c>] paginated saved task status (--view archived: put away)
|
|
279
|
+
toolroll task archive <id...> [--reason <why>] put tasks away: out of every list, history kept (a person's act)
|
|
280
|
+
toolroll task restore <id...> bring put-away tasks back exactly as they were
|
|
281
|
+
toolroll task tidy --preview [--days N] what Put away old tasks would put away, and why (read-only)
|
|
282
|
+
toolroll task tidy --apply --digest <d> [--except <id,...>] put away exactly that list
|
|
276
283
|
toolroll task show <id>
|
|
277
284
|
toolroll task wait <id> [--timeout <seconds>]
|
|
278
285
|
wait for this attempt to finish or need a person
|
|
279
286
|
toolroll project use <path> remember a saved project (optional --token-file)
|
|
280
287
|
toolroll project show show the current project and credential reference
|
|
288
|
+
toolroll project activity --json read the current project supervision report
|
|
281
289
|
toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
|
|
282
290
|
--not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
|
|
283
291
|
toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
|
|
@@ -661,7 +669,7 @@ export const TASK_ACTIONS = [
|
|
|
661
669
|
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign",
|
|
662
670
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
663
671
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask",
|
|
664
|
-
"checks", "add-tests",
|
|
672
|
+
"checks", "add-tests", "archive", "restore", "tidy",
|
|
665
673
|
];
|
|
666
674
|
export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
667
675
|
export const CONFIG_ACTIONS = ["show", "set", "clear"];
|
|
@@ -705,6 +713,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
705
713
|
"starter",
|
|
706
714
|
// flows: templates, steps, scripts and cards.
|
|
707
715
|
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone", "option",
|
|
716
|
+
// task tidy --apply: the previewed tasks to leave out.
|
|
717
|
+
"except",
|
|
708
718
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
709
719
|
// pings follow responsibility: replacements.
|
|
710
720
|
"replaced-by", "replaces",
|
|
@@ -755,6 +765,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
755
765
|
"password-stdin",
|
|
756
766
|
// task steer: pause the running Claude build, then send the note.
|
|
757
767
|
"now",
|
|
768
|
+
// task tidy: look first (--preview), then put away exactly what was shown (--apply --digest).
|
|
769
|
+
"preview", "apply",
|
|
758
770
|
]);
|
|
759
771
|
/** `project network` reads --remove <hosts> and --local any|none as values, where every other verb's --remove and
|
|
760
772
|
* --local are switches. */
|
|
@@ -1562,6 +1574,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1562
1574
|
return fail(write, json, "claim", "sweep-limit", `its sweep already has ${result.limit} running at once — it starts when one finishes`, EXIT.refused, { sweep: result.sweep, limit: result.limit });
|
|
1563
1575
|
if (result.reason === "watch-custody")
|
|
1564
1576
|
return fail(write, json, "claim", "watch-custody", "This worker no longer holds the project; restart the watch", EXIT.refused);
|
|
1577
|
+
if (result.reason === "archived")
|
|
1578
|
+
return fail(write, json, "claim", "archived", `${id} is put away — restore it with \`toolroll task restore ${id}\` first`, EXIT.refused);
|
|
1565
1579
|
if (result.reason === "draining")
|
|
1566
1580
|
return fail(write, json, "claim", "draining", `Toolroll is restarting (draining): ${runner} takes no new work until it is back (by ${result.until})`, EXIT.refused, { until: result.until });
|
|
1567
1581
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
@@ -2259,6 +2273,7 @@ function flowIoOf(context, pool, git, base) {
|
|
|
2259
2273
|
dir: context.flowStepIo?.dir ?? dir, scratch: context.flowStepIo?.scratch ?? join(pool, "flow-checks"), base,
|
|
2260
2274
|
...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }) },
|
|
2261
2275
|
...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }),
|
|
2276
|
+
youiyouxGit: gitOf(context.gitRunner),
|
|
2262
2277
|
};
|
|
2263
2278
|
}
|
|
2264
2279
|
/**
|
|
@@ -3384,8 +3399,10 @@ async function tickCommand(flags, context) {
|
|
|
3384
3399
|
syncAssignmentHandoffs(store, clock(), auth.runner.repos, context.evidenceRoot);
|
|
3385
3400
|
settleFinished();
|
|
3386
3401
|
const replied = replyPass.taken > 0 || replyPass.problem !== null;
|
|
3387
|
-
const
|
|
3388
|
-
|
|
3402
|
+
const requestPass = housekeeping?.youiyoux ?? NO_YOUIYOUX_PASS;
|
|
3403
|
+
const requested = requestPass.filed + requestPass.copied + requestPass.decided + requestPass.problems.length > 0;
|
|
3404
|
+
const flows = flowPass.moved + flowPass.filed.length + flowPass.problems.length + triggerPass.added + triggerPass.problems.length + stepPass.ran + stepPass.problems.length === 0 && !replied && !requested ? {} : { flows: { ...flowPass,
|
|
3405
|
+
...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(requested ? { youiyoux: requestPass } : {}), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
|
|
3389
3406
|
const summary = () => {
|
|
3390
3407
|
const lines = [`Considered ${considered}, built ${built}, parked ${parked}, broke ${broke}.`];
|
|
3391
3408
|
for (const entry of dispatched) {
|
|
@@ -3557,6 +3574,13 @@ async function reconcileCommand(flags, context) {
|
|
|
3557
3574
|
remotes.close();
|
|
3558
3575
|
}
|
|
3559
3576
|
const landedGone = landed.steps.filter(one => one.outcome === "removed");
|
|
3577
|
+
// Tidy (task-tidy.md): old routine runs a newer run replaces, release tasks already published or replaced, and CI
|
|
3578
|
+
// notices main has fixed are put away, at most a bounded number a pass, each with its reason in the ledger.
|
|
3579
|
+
let tidied = [];
|
|
3580
|
+
try {
|
|
3581
|
+
tidied = runTidy(store, clock(), releaseFactsFor(context.databaseFile), TIDY_PASS_LIMIT, { principal: "operator", repos: [repo], includeUnplaced: false });
|
|
3582
|
+
}
|
|
3583
|
+
catch { /* the next pass tries again */ }
|
|
3560
3584
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
3561
3585
|
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
3562
3586
|
// Completing or cancelling a task stops anything still running from its checkout first.
|
|
@@ -3600,6 +3624,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3600
3624
|
slimmed.slimmed.length === 0 &&
|
|
3601
3625
|
landed.completed.length === 0 &&
|
|
3602
3626
|
landedGone.length === 0 &&
|
|
3627
|
+
tidied.length === 0 &&
|
|
3603
3628
|
liveSwept.removed.length === 0;
|
|
3604
3629
|
return succeed(write, json, "reconcile", {
|
|
3605
3630
|
recovered,
|
|
@@ -3611,6 +3636,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3611
3636
|
processesStopped: [...closedRuns.map(one => ({ run: one.runId, pids: one.done.stopped.map(row => row.pid) })), ...closedCheckouts.map(one => ({ checkout: one.path, pids: one.done.stopped.map(row => row.pid) }))],
|
|
3612
3637
|
checkoutsSlimmed: slimmed.slimmed.map(one => one.path),
|
|
3613
3638
|
landed: { completed: landed.completed, steps: landed.steps },
|
|
3639
|
+
tidied: tidied.map(one => ({ id: one.taskId, rule: one.rule, reason: one.reason })),
|
|
3614
3640
|
liveViewsSwept: liveSwept.removed.length,
|
|
3615
3641
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
3616
3642
|
storageSweep: storageSwept,
|
|
@@ -3625,6 +3651,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3625
3651
|
...closedRuns.map(one => `Run #${one.runId} ended; stopped ${stoppedWords(one.done)}.`),
|
|
3626
3652
|
...closedCheckouts.map(one => `${one.path}: its task finished; stopped ${stoppedWords(one.done)}.`),
|
|
3627
3653
|
...(landed.completed.length === 0 ? [] : [`Marked ${landed.completed.length} landed result(s) complete: ${landed.completed.join(", ")}.`]),
|
|
3654
|
+
...(tidied.length === 0 ? [] : [`Put away ${tidied.length} old task(s): ${tidied.map(one => one.taskId).join(", ")}.`]),
|
|
3628
3655
|
...(landedGone.length === 0 ? [] : [`Removed landed work: ${landedGone.filter(one => one.kind === "checkout").length} checkout(s), ${landedGone.filter(one => one.kind === "branch").length} local branch(es).`]),
|
|
3629
3656
|
...(slimmed.slimmed.length === 0 ? [] : [`Dropped dependencies and build output from ${slimmed.slimmed.length} kept checkout(s), about ${bytesWords(slimmed.slimmed.reduce((sum, one) => sum + one.bytes, 0))}.`]),
|
|
3630
3657
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
@@ -4107,6 +4134,7 @@ async function requeueTask(positional, flags, context) {
|
|
|
4107
4134
|
published: `${id}'s last result is published; revise it through its pull request`,
|
|
4108
4135
|
// Only an Approve & build again asks for an exact build; the CLI never does.
|
|
4109
4136
|
moved: `${id} changed since its page was read; read it again`,
|
|
4137
|
+
archived: `${id} is put away — restore it with \`toolroll task restore ${id}\` first`,
|
|
4110
4138
|
};
|
|
4111
4139
|
return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
|
|
4112
4140
|
}
|
|
@@ -6396,7 +6424,7 @@ async function runWatchLoop(args) {
|
|
|
6396
6424
|
base = head.stdout.trim();
|
|
6397
6425
|
}
|
|
6398
6426
|
const pass = await flowHousekeeping(store, repo, context.clock, flowIoOf(context, flowPool, flowGit, base), admissionStopped);
|
|
6399
|
-
flowsDid(pass.flows.moved + pass.triggers.added + pass.steps.ran + pass.replies.taken, pass.flows.filed.length, `flow pass — ${pass.triggers.added} card(s) added, ${pass.replies.taken} reply(ies) taken, ${pass.steps.ran} step(s) run, ${pass.flows.moved} moved`);
|
|
6427
|
+
flowsDid(pass.flows.moved + pass.triggers.added + pass.steps.ran + pass.replies.taken, pass.flows.filed.length + pass.youiyoux.filed, `flow pass — ${pass.triggers.added} card(s) added, ${pass.youiyoux.filed} youiyoux request(s) filed, ${pass.replies.taken} reply(ies) taken, ${pass.steps.ran} step(s) run, ${pass.flows.moved} moved`);
|
|
6400
6428
|
},
|
|
6401
6429
|
});
|
|
6402
6430
|
const modelWatch = startMaintenance({
|
|
@@ -6936,8 +6964,6 @@ export function openBrowser(url) {
|
|
|
6936
6964
|
// A browser that will not open is a URL the greeting already printed.
|
|
6937
6965
|
}
|
|
6938
6966
|
}
|
|
6939
|
-
/** Where `up` last served its console, kept beside the database so `onboard` names the same address. */
|
|
6940
|
-
const UP_CONSOLE_FILE = "up-console.json";
|
|
6941
6967
|
function recordUpConsoleUrl(dir, url, now) {
|
|
6942
6968
|
try {
|
|
6943
6969
|
const file = join(dir, UP_CONSOLE_FILE), temporary = `${file}.${process.pid}.tmp`;
|
|
@@ -6948,15 +6974,6 @@ function recordUpConsoleUrl(dir, url, now) {
|
|
|
6948
6974
|
// A display fact: a failed write only leaves onboard on the default port.
|
|
6949
6975
|
}
|
|
6950
6976
|
}
|
|
6951
|
-
export function readUpConsoleUrl(dir) {
|
|
6952
|
-
try {
|
|
6953
|
-
const parsed = JSON.parse(readFileSync(join(dir, UP_CONSOLE_FILE), "utf8"));
|
|
6954
|
-
return typeof parsed.url === "string" && /^https?:\/\/[^\s]+$/.test(parsed.url) ? parsed.url.replace(/\/+$/, "") : null;
|
|
6955
|
-
}
|
|
6956
|
-
catch {
|
|
6957
|
-
return null;
|
|
6958
|
-
}
|
|
6959
|
-
}
|
|
6960
6977
|
/** How onboard says to sign in: the saved login file only while it still works (as `up` advertises it), else the
|
|
6961
6978
|
* account and its password; null only when there is no account yet. Never the password itself. */
|
|
6962
6979
|
function onboardLogin(store, loginFile) {
|
|
@@ -7041,6 +7058,8 @@ async function onboardCommand(flags, context) {
|
|
|
7041
7058
|
return `off: ${checked.message}`;
|
|
7042
7059
|
return `off: turn on with toolroll publish setup --repo ${repo} --yes (asks your password), or in Settings → Projects`;
|
|
7043
7060
|
}),
|
|
7061
|
+
// A project that uses youiyoux has its change requests filed as tasks with no step of its own (youiyoux-intake.ts).
|
|
7062
|
+
youiyoux: seams.youiyoux ?? (async (repo) => youiyouxOnboardLine(store, repo)),
|
|
7044
7063
|
starterFlag: text(flags, "starter"),
|
|
7045
7064
|
// Starter flows belong to a person: the flow's decisions ask them. With no account yet, none can be switched on.
|
|
7046
7065
|
starters: {
|
|
@@ -8571,6 +8590,12 @@ function taskAction(action, rest, flags, context) {
|
|
|
8571
8590
|
return addTestsTask(rest, flags, context);
|
|
8572
8591
|
case "list":
|
|
8573
8592
|
return listTasks(flags, context);
|
|
8593
|
+
case "archive":
|
|
8594
|
+
return archiveTasksCommand(rest, flags, context);
|
|
8595
|
+
case "restore":
|
|
8596
|
+
return restoreTasksCommand(rest, flags, context);
|
|
8597
|
+
case "tidy":
|
|
8598
|
+
return tidyTasksCommand(rest, flags, context);
|
|
8574
8599
|
case "show":
|
|
8575
8600
|
return showTask(rest, context);
|
|
8576
8601
|
case "wait":
|
|
@@ -8992,9 +9017,9 @@ function listTasks(flags, context) {
|
|
|
8992
9017
|
const limit = rawLimit === undefined ? 40 : Number(rawLimit);
|
|
8993
9018
|
if (wanted !== undefined && !STATES.includes(wanted))
|
|
8994
9019
|
return fail(write, json, 'task list', 'usage', `--state takes one of ${STATES.join(', ')}`, EXIT.usage);
|
|
8995
|
-
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed'].includes(rawView) ||
|
|
9020
|
+
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed', 'archived'].includes(rawView) ||
|
|
8996
9021
|
!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
8997
|
-
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running or
|
|
9022
|
+
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running, completed or archived; --limit takes 1–100', EXIT.usage);
|
|
8998
9023
|
}
|
|
8999
9024
|
// A remote person limited to some projects (remoteLensOf) pages and counts only theirs.
|
|
9000
9025
|
const lens = context.principal?.lens ?? null;
|
|
@@ -9021,7 +9046,8 @@ function listTasks(flags, context) {
|
|
|
9021
9046
|
write(wanted === undefined ? 'The task list is empty.' : `Nothing is ${wanted}.`);
|
|
9022
9047
|
const width = Math.max(0, ...tasks.map(task => task.id.length));
|
|
9023
9048
|
for (const task of tasks)
|
|
9024
|
-
write(` ${task.id.padEnd(width)} ${task.status.label.padEnd(16)} ${task.title}`
|
|
9049
|
+
write(task.archived === undefined ? ` ${task.id.padEnd(width)} ${task.status.label.padEnd(16)} ${task.title}`
|
|
9050
|
+
: ` ${task.id.padEnd(width)} ${'Archived'.padEnd(16)} ${task.title} — by ${task.archived.byKind === 'tidy' ? 'tidy' : task.archived.by}: ${task.archived.reason}`);
|
|
9025
9051
|
if (page.nextCursor !== null)
|
|
9026
9052
|
write(`Next page: --cursor ${page.nextCursor}`);
|
|
9027
9053
|
return EXIT.ok;
|
|
@@ -10153,6 +10179,8 @@ function describeApproveFailure(reason, id) {
|
|
|
10153
10179
|
// Approval policy, phase 1: the lead's token (or a remote API token) proves a credential, not a person.
|
|
10154
10180
|
if (reason === "machine-proof")
|
|
10155
10181
|
return "only a person can approve this — approve it on the task page or from your paired chat";
|
|
10182
|
+
if (reason === "archived")
|
|
10183
|
+
return `${id} is put away — restore it with \`toolroll task restore ${id}\` first`;
|
|
10156
10184
|
if (reason === "second-approver")
|
|
10157
10185
|
return `your approval of ${id} is recorded; it is protected work, so a second person needs to approve it (toolroll task approve ${id} as them)`;
|
|
10158
10186
|
if (reason === "no-approvers") {
|
|
@@ -12758,3 +12786,180 @@ export function consoleIdentitiesInBackground(identities, paths) {
|
|
|
12758
12786
|
export function writeModelRow(store, proved, scope, knownProject, slot, to, now, extra = []) {
|
|
12759
12787
|
return applyModelChoices(store, { scope, change: slot === null ? [] : [{ slot, to }], expected: null, strict: false, knownProject, extra }, { ...proved, live: () => true }, now);
|
|
12760
12788
|
}
|
|
12789
|
+
/** Putting away and restoring are a person's acts (docs/plans/task-tidy.md): never the lead's token, never a remote
|
|
12790
|
+
* API token, never a machine proof. Null when a person is acting. */
|
|
12791
|
+
function refuseMachineTidy(context, command, method) {
|
|
12792
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || (method !== undefined && !personProof(method))) {
|
|
12793
|
+
return fail(context.write, context.json, command, "person-only", "Only a person puts tasks away or restores them. Use the console or your own login.", EXIT.refused);
|
|
12794
|
+
}
|
|
12795
|
+
return null;
|
|
12796
|
+
}
|
|
12797
|
+
/** `toolroll task archive <id...> [--reason <text>]`: put tasks away by hand (v145). Each keeps its history, results and
|
|
12798
|
+
* approvals; nothing can run, claim or approve it until it is restored. Credentialed, a person's act. */
|
|
12799
|
+
async function archiveTasksCommand(positional, flags, context) {
|
|
12800
|
+
const { store, write, json, clock } = context;
|
|
12801
|
+
const command = "task archive";
|
|
12802
|
+
const ids = [...new Set(positional)];
|
|
12803
|
+
if (ids.length === 0 || ids.length > 100)
|
|
12804
|
+
return fail(write, json, command, "usage", "`toolroll task archive <id...> [--reason <why>] --as <you> --token <t>` — 1 to 100 tasks", EXIT.usage);
|
|
12805
|
+
const machine = refuseMachineTidy(context, command);
|
|
12806
|
+
if (machine !== null)
|
|
12807
|
+
return machine;
|
|
12808
|
+
const demo = refuseDemo(context, command);
|
|
12809
|
+
if (demo !== null)
|
|
12810
|
+
return demo;
|
|
12811
|
+
const acting = await askCredentials(flags, context);
|
|
12812
|
+
// The password is proved against a project the person can see (the first task's); each task's own project is
|
|
12813
|
+
// re-proved inside its put-away transaction.
|
|
12814
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, firstVisibleRepo(store, acting.name, ids));
|
|
12815
|
+
if (acting === null || !verified?.ok)
|
|
12816
|
+
return refuseApprover(context, command, verified, "putting a task away takes `--as <you> --token <t>` — it is recorded against your name", ids[0]);
|
|
12817
|
+
const personal = refuseMachineTidy(context, command, verified.method);
|
|
12818
|
+
if (personal !== null)
|
|
12819
|
+
return personal;
|
|
12820
|
+
const reason = text(flags, "reason") ?? "Put away by hand";
|
|
12821
|
+
const archived = [], refused = [];
|
|
12822
|
+
for (const id of ids) {
|
|
12823
|
+
const ref = store.lookupRef(id);
|
|
12824
|
+
if (ref === null || store.getTask(id) === null || !store.accountCanAccess(acting.name, ref.repo)) {
|
|
12825
|
+
refused.push({ id, reason: "unknown-task", message: `No task ${id}.` });
|
|
12826
|
+
continue;
|
|
12827
|
+
}
|
|
12828
|
+
const done = archiveTaskFamily(store, id, { by: acting.name, byKind: "person", rule: null, reason }, clock(), { guard: refs => approverStandsNow(store, acting.name, familyRepoOf(store, refs)) });
|
|
12829
|
+
if (done.ok)
|
|
12830
|
+
archived.push({ id, versions: done.refs.length });
|
|
12831
|
+
else
|
|
12832
|
+
refused.push({ id, reason: done.reason, message: done.message });
|
|
12833
|
+
}
|
|
12834
|
+
const lines = () => [
|
|
12835
|
+
...archived.map(one => `Put away ${one.id}${one.versions > 1 ? ` (${one.versions} versions)` : ""}.`),
|
|
12836
|
+
...refused.map(one => `Not put away: ${one.message}`),
|
|
12837
|
+
...(archived.length === 0 ? [] : [`Restore with \`toolroll task restore ${archived.map(one => one.id).join(" ")}\`.`]),
|
|
12838
|
+
];
|
|
12839
|
+
if (refused.length === 0)
|
|
12840
|
+
return succeed(write, json, command, { archived, refused }, lines);
|
|
12841
|
+
if (json)
|
|
12842
|
+
return fail(write, json, command, refused[0].reason, refused[0].message, EXIT.refused, { archived, refused });
|
|
12843
|
+
write(lines().join("\n"));
|
|
12844
|
+
return EXIT.refused;
|
|
12845
|
+
}
|
|
12846
|
+
/** `toolroll task restore <id...>`: bring put-away tasks back exactly as they were (v145). Credentialed, a person's act. */
|
|
12847
|
+
async function restoreTasksCommand(positional, flags, context) {
|
|
12848
|
+
const { store, write, json, clock } = context;
|
|
12849
|
+
const command = "task restore";
|
|
12850
|
+
const ids = [...new Set(positional)];
|
|
12851
|
+
if (ids.length === 0 || ids.length > 100)
|
|
12852
|
+
return fail(write, json, command, "usage", "`toolroll task restore <id...> --as <you> --token <t>` — 1 to 100 tasks", EXIT.usage);
|
|
12853
|
+
const machine = refuseMachineTidy(context, command);
|
|
12854
|
+
if (machine !== null)
|
|
12855
|
+
return machine;
|
|
12856
|
+
const demo = refuseDemo(context, command);
|
|
12857
|
+
if (demo !== null)
|
|
12858
|
+
return demo;
|
|
12859
|
+
const acting = await askCredentials(flags, context);
|
|
12860
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, firstVisibleRepo(store, acting.name, ids));
|
|
12861
|
+
if (acting === null || !verified?.ok)
|
|
12862
|
+
return refuseApprover(context, command, verified, "restoring takes `--as <you> --token <t>` — it is recorded against your name", ids[0]);
|
|
12863
|
+
const personal = refuseMachineTidy(context, command, verified.method);
|
|
12864
|
+
if (personal !== null)
|
|
12865
|
+
return personal;
|
|
12866
|
+
const restored = [], refused = [];
|
|
12867
|
+
for (const id of ids) {
|
|
12868
|
+
const ref = store.lookupRef(id);
|
|
12869
|
+
if (ref === null || store.getTask(id) === null || !store.accountCanAccess(acting.name, ref.repo)) {
|
|
12870
|
+
refused.push({ id, reason: "unknown-task", message: `No task ${id}.` });
|
|
12871
|
+
continue;
|
|
12872
|
+
}
|
|
12873
|
+
const done = restoreTaskFamily(store, id, acting.name, clock(), refs => approverStandsNow(store, acting.name, familyRepoOf(store, refs)));
|
|
12874
|
+
if (done.ok)
|
|
12875
|
+
restored.push(id);
|
|
12876
|
+
else
|
|
12877
|
+
refused.push({ id, reason: done.reason, message: done.message });
|
|
12878
|
+
}
|
|
12879
|
+
const lines = () => [...restored.map(id => `Restored ${id}.`), ...refused.map(one => `Not restored: ${one.message}`)];
|
|
12880
|
+
if (refused.length === 0)
|
|
12881
|
+
return succeed(write, json, command, { restored, refused }, lines);
|
|
12882
|
+
if (json)
|
|
12883
|
+
return fail(write, json, command, refused[0].reason, refused[0].message, EXIT.refused, { restored, refused });
|
|
12884
|
+
write(lines().join("\n"));
|
|
12885
|
+
return EXIT.refused;
|
|
12886
|
+
}
|
|
12887
|
+
/** `toolroll task tidy --preview [--days N]` (read-only, no credential) and `toolroll task tidy --apply --digest <d>
|
|
12888
|
+
* [--days N] [--except <id,...>]` (credentialed, a person's act): the console's Put away old tasks, word for word,
|
|
12889
|
+
* with the same preview and digest. Apply puts away exactly what the preview with that digest showed. */
|
|
12890
|
+
async function tidyTasksCommand(positional, flags, context) {
|
|
12891
|
+
const { store, write, json, clock } = context;
|
|
12892
|
+
const command = "task tidy";
|
|
12893
|
+
const preview = flag(flags, "preview"), apply = flag(flags, "apply");
|
|
12894
|
+
const days = tidyDays(text(flags, "days"));
|
|
12895
|
+
const digest = text(flags, "digest");
|
|
12896
|
+
const except = (text(flags, "except") ?? "").split(",").map(one => one.trim()).filter(one => one !== "");
|
|
12897
|
+
const usage = "`toolroll task tidy --preview [--days N]`, then `toolroll task tidy --apply --digest <d> [--days N] [--except <id,...>]`";
|
|
12898
|
+
if (positional.length > 0 || preview === apply || days === null || (preview && (digest !== undefined || except.length > 0)) || (apply && digest === undefined))
|
|
12899
|
+
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
12900
|
+
const releases = releaseFactsFor(context.databaseFile);
|
|
12901
|
+
// One scope for the preview and the apply: `--as <you>` previews exactly your projects (no password: it reads), and
|
|
12902
|
+
// the apply reads the same person's projects, so an unchanged list keeps its digest.
|
|
12903
|
+
// Whose lists: `--as <you>`, else this machine's saved login (`toolroll up`), else everything (an unrestricted
|
|
12904
|
+
// person's list; a person limited to some projects previews with --as).
|
|
12905
|
+
const previewAs = text(flags, "as") ?? readLoginFile(join(dirname(context.databaseFile), UP_LOGIN_FILE))?.name;
|
|
12906
|
+
const daysArg = `${days === TIDY_DEFAULT_DAYS ? "" : ` --days ${days}`}${previewAs === undefined ? "" : ` --as ${previewAs}`}`;
|
|
12907
|
+
// `--as` only ever narrows: the named person's projects within the caller's own lens, never beyond it.
|
|
12908
|
+
const lens = context.principal?.lens ?? null;
|
|
12909
|
+
const own = { principal: "operator", repos: lens, includeUnplaced: lens === null };
|
|
12910
|
+
const access = previewAs !== undefined ? withinLens(accessOf(store, previewAs), lens) : own;
|
|
12911
|
+
if (preview) {
|
|
12912
|
+
const items = tidyPreview(store, clock(), { releases, days, access });
|
|
12913
|
+
const shown = tidyDigest(items, days);
|
|
12914
|
+
const groups = tidyGroups(items, days);
|
|
12915
|
+
const next = `toolroll task tidy --apply --digest ${shown}${daysArg}`;
|
|
12916
|
+
return succeed(write, json, command, { preview: true, days, digest: shown, count: items.length, groups, next: items.length === 0 ? null : next,
|
|
12917
|
+
...(releases.problem === null ? {} : { releasesSkipped: releases.problem }) }, () => items.length === 0
|
|
12918
|
+
? ["Nothing to put away."]
|
|
12919
|
+
: [`Put away old tasks: ${items.length} task${items.length === 1 ? "" : "s"}.`,
|
|
12920
|
+
...groups.flatMap(group => ["", `${group.label} (${group.items.length})`, ...group.items.map(one => ` ${one.taskId} ${one.reason}${one.members.length > 1 ? ` (${one.members.length} versions)` : ""}`)]),
|
|
12921
|
+
...(releases.problem === null ? [] : ["", `Release tasks were left out: ${releases.problem}.`]),
|
|
12922
|
+
"", `To put these away, run: ${next}`, "Leave some out with --except <id,...>.",
|
|
12923
|
+
...(previewAs === undefined ? ["Limited to some projects? Preview with --as <you> so the list is yours."] : [])]);
|
|
12924
|
+
}
|
|
12925
|
+
const machine = refuseMachineTidy(context, command);
|
|
12926
|
+
if (machine !== null)
|
|
12927
|
+
return machine;
|
|
12928
|
+
const demo = refuseDemo(context, command);
|
|
12929
|
+
if (demo !== null)
|
|
12930
|
+
return demo;
|
|
12931
|
+
const acting = await askCredentials(flags, context);
|
|
12932
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, firstVisibleRepo(store, acting.name, []));
|
|
12933
|
+
if (acting === null || !verified?.ok)
|
|
12934
|
+
return refuseApprover(context, command, verified, "putting tasks away takes `--as <you> --token <t>` — it is recorded against your name", undefined);
|
|
12935
|
+
const personal = refuseMachineTidy(context, command, verified.method);
|
|
12936
|
+
if (personal !== null)
|
|
12937
|
+
return personal;
|
|
12938
|
+
// The person's own view of the lists, from the verified credential (never a flag), within the caller's lens: the
|
|
12939
|
+
// same scope `--preview --as <them>` read.
|
|
12940
|
+
const mine = withinLens(accessOf(store, acting.name), lens);
|
|
12941
|
+
const shownItems = tidyPreview(store, clock(), { releases, days, access: mine });
|
|
12942
|
+
const only = except.length === 0 ? null : shownItems.map(one => one.taskId).filter(id => !except.includes(id));
|
|
12943
|
+
if (except.some(id => !shownItems.some(one => one.taskId === id)))
|
|
12944
|
+
return fail(write, json, command, "outside", "--except names a task that isn't in the list. Check it again with --preview.", EXIT.refused);
|
|
12945
|
+
const done = applyTidy(store, clock(), { by: acting.name, days, digest: digest, only, releases, access: mine });
|
|
12946
|
+
if (!done.ok) {
|
|
12947
|
+
const again = `toolroll task tidy --apply --digest ${done.digest}${daysArg}`;
|
|
12948
|
+
return fail(write, json, command, done.reason, done.reason === "changed" ? `${done.message} Look again with \`toolroll task tidy --preview${daysArg}\`.` : done.message, EXIT.refused, { digest: done.digest, count: done.items.length, ...(done.reason === "changed" ? { next: done.items.length === 0 ? null : again } : {}) });
|
|
12949
|
+
}
|
|
12950
|
+
return succeed(write, json, command, { archived: done.archived.map(one => ({ id: one.taskId, rule: one.rule, reason: one.reason })), skipped: done.skipped }, () => [
|
|
12951
|
+
`Put away ${done.archived.length} task${done.archived.length === 1 ? "" : "s"}.`,
|
|
12952
|
+
...done.skipped.map(one => `Not put away: ${one.message}`),
|
|
12953
|
+
"Find them with `toolroll task list --view archived`; bring one back with `toolroll task restore <id>`.",
|
|
12954
|
+
]);
|
|
12955
|
+
}
|
|
12956
|
+
/** The project a put-away or restore proves the person's password against: the first named task's, when they can see
|
|
12957
|
+
* it; otherwise the first project they can see at all (null when none: an unrestricted person, or none to prove). */
|
|
12958
|
+
function firstVisibleRepo(store, name, ids) {
|
|
12959
|
+
for (const id of ids) {
|
|
12960
|
+
const repo = store.lookupRef(id)?.repo ?? null;
|
|
12961
|
+
if (repo !== null && store.accountCanAccess(name, repo))
|
|
12962
|
+
return repo;
|
|
12963
|
+
}
|
|
12964
|
+
return store.accountCanAccess(name, null) ? null : store.knownRepos().find(repo => store.accountCanAccess(name, repo)) ?? null;
|
|
12965
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Shared presentation for browser and no-script Activity; facts come only
|
|
2
|
+
* from projectOutcomes. No alternate counting or status interpretation. */
|
|
3
|
+
import type { ActivityFamily, ProjectOutcomes } from './project-outcomes.js';
|
|
4
|
+
export declare const activityAge: (ms: number | null) => string;
|
|
5
|
+
export declare const activityMoney: (usd: number | null) => string;
|
|
6
|
+
export declare const ACTIVITY_ACTORS: {
|
|
7
|
+
human: string;
|
|
8
|
+
lead: string;
|
|
9
|
+
landed: string;
|
|
10
|
+
'machine-or-unknown': string;
|
|
11
|
+
};
|
|
12
|
+
export type ActivityMetric = {
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
detail: string;
|
|
16
|
+
rows: ActivityFamily[];
|
|
17
|
+
};
|
|
18
|
+
export declare const activityScope: (report: ProjectOutcomes) => string;
|
|
19
|
+
export declare const activityLimit: (report: ProjectOutcomes) => string;
|
|
20
|
+
export declare function activityMetrics(report: ProjectOutcomes): ActivityMetric[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const activityAge = (ms) => ms === null ? 'Unknown' : ms < 3600000 ? `${Math.floor(ms / 60000)}m` : ms < 86400000 ? `${(ms / 3600000).toFixed(1)}h` : `${(ms / 86400000).toFixed(1)}d`;
|
|
2
|
+
export const activityMoney = (usd) => usd === null ? 'Unknown' : `$${usd.toFixed(2)}`;
|
|
3
|
+
export const ACTIVITY_ACTORS = { human: 'Human', lead: 'Lead', landed: 'Automatically landed', 'machine-or-unknown': 'Machine or unknown' };
|
|
4
|
+
export const activityScope = (report) => `Current Ready backlog; completions in the last ${report.days} days. Completion does not mean checks passed or work deployed.`;
|
|
5
|
+
export const activityLimit = (report) => `Partial report: only the ${report.limit} most recently updated eligible families are included. Counts and coverage below describe this sample.`;
|
|
6
|
+
export function activityMetrics(report) {
|
|
7
|
+
const ready = report.families.filter(one => one.state === 'ready');
|
|
8
|
+
const complete = report.families.filter(one => one.state === 'completed');
|
|
9
|
+
const metrics = [
|
|
10
|
+
{ label: 'Ready', value: report.ready.count === 0 ? '0' : `${report.ready.count} · Oldest ${activityAge(report.ready.oldestFinishedAgeMs)}`,
|
|
11
|
+
detail: `Age is measured from the latest result finishing, a proxy for Ready entry. ${report.ready.dated}/${report.ready.count} have usable timestamps.`, rows: ready },
|
|
12
|
+
{ label: 'Finished to decision', value: report.completed.count === 0 ? 'No completed families' : `${activityAge(report.finishedToDecision.meanMs)} average`,
|
|
13
|
+
detail: `${report.finishedToDecision.samples}/${report.completed.count} completed families have usable timestamps. Elapsed time to the saved completion decision, not human work time.`, rows: complete },
|
|
14
|
+
{ label: 'Explicit revisions', value: report.revisions.perCompletedFamily === null ? 'No completed families' : `${report.revisions.perCompletedFamily.toFixed(1)} per completed family`,
|
|
15
|
+
detail: `${report.revisions.count} revisions across ${report.revisions.samples} completed families. Retries are attempts, not revisions.`, rows: complete },
|
|
16
|
+
{ label: 'Recorded spend', value: report.completed.count === 0 ? 'No completed families' : `${activityMoney(report.cost.recordedPerCompletedFamilyUsd)} per completed family · ${report.cost.knownRuns}/${report.cost.runs} costs known`,
|
|
17
|
+
detail: `${activityMoney(report.cost.recordedUsd)} in API charges across ${report.cost.families} completed families; ${report.cost.fullyCostedFamilies}/${report.cost.families} fully costed.${report.cost.subscriptionRuns > 0 ? ` ${report.cost.subscriptionRuns} subscription runs are plan usage, not charges${report.cost.subscriptionReportedUsd === null ? '' : ` (${activityMoney(report.cost.subscriptionReportedUsd)} reported)`}.` : ''} Missing or unreported costs are unknown. All family attempts count once, including planning and retries.`, rows: complete },
|
|
18
|
+
{ label: 'Completed families', value: String(report.completed.count),
|
|
19
|
+
detail: `${Object.entries(report.completed.actors).map(([key, count]) => `${ACTIVITY_ACTORS[key]}: ${count}`).join(' · ')}. Scouts: ${report.completed.scouts}. Failed checks: ${report.completed.failedChecks}.`, rows: complete },
|
|
20
|
+
...(report.incompleteHistory > 0 ? [{ label: 'Incomplete history', value: String(report.incompleteHistory), detail: 'Excluded from completion averages; their family relationships cannot be established.', rows: report.families.filter(one => one.state === 'history-unavailable') }] : []),
|
|
21
|
+
];
|
|
22
|
+
return report.completed.count === 0 ? metrics.filter(one => one.label === 'Ready' || one.label === 'Completed families' || one.label === 'Incomplete history') : metrics;
|
|
23
|
+
}
|
package/dist/project-cli.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare function runProjectCommand(positional: readonly string[], flags:
|
|
|
12
12
|
write: (line: string) => void;
|
|
13
13
|
json: boolean;
|
|
14
14
|
registryFile?: string;
|
|
15
|
+
now?: Date;
|
|
15
16
|
}): Promise<number>;
|
|
16
17
|
/** Only known credential-aware adapters consume the remembered reference.
|
|
17
18
|
* Explicit credentials always win; no legacy action gains implicit authority. */
|
package/dist/project-cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { configPath, loadRepos } from './repos.js';
|
|
|
8
8
|
import { authenticateCoordinator } from './coordinator.js';
|
|
9
9
|
import { canonicalProject } from './project.js';
|
|
10
10
|
import { envelopeJson } from './envelope.js';
|
|
11
|
+
import { projectOutcomes } from './project-outcomes.js';
|
|
11
12
|
export const projectProfilePath = (databaseFile) => join(dirname(databaseFile), 'cli-project.json');
|
|
12
13
|
export function readProjectProfile(databaseFile) {
|
|
13
14
|
const file = projectProfilePath(databaseFile);
|
|
@@ -25,11 +26,29 @@ export function readProjectProfile(databaseFile) {
|
|
|
25
26
|
throw new Error('The saved CLI project is invalid. Select it again with project use.');
|
|
26
27
|
return { version: 1, repo: p['repo'], tokenFile: p['tokenFile'] };
|
|
27
28
|
}
|
|
29
|
+
function scopedCredentialRefusal(store, tokenFile, repo) {
|
|
30
|
+
let token;
|
|
31
|
+
try {
|
|
32
|
+
const info = statSync(tokenFile);
|
|
33
|
+
if (!info.isFile() || info.size > 16_384)
|
|
34
|
+
return { reason: 'unauthenticated', message: 'The scoped credential file is unavailable.' };
|
|
35
|
+
token = readFileSync(tokenFile, 'utf8').trim();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { reason: 'unauthenticated', message: 'The scoped credential file is unavailable.' };
|
|
39
|
+
}
|
|
40
|
+
const auth = authenticateCoordinator(store, token);
|
|
41
|
+
if (!auth.ok)
|
|
42
|
+
return { reason: 'unauthenticated', message: 'The scoped credential is invalid or revoked.' };
|
|
43
|
+
if (!auth.who.repos.includes(repo))
|
|
44
|
+
return { reason: 'not-found', message: 'That project is unavailable to this credential.' };
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
28
47
|
export async function runProjectCommand(positional, flags, context) {
|
|
29
48
|
const action = positional[0] ?? 'show', command = `project ${action}`;
|
|
30
49
|
const fail = (reason, message) => { context.write(context.json ? envelopeJson({ ok: false, command, reason, message }) : message); return reason === 'usage' ? 2 : 3; };
|
|
31
|
-
if (!['use', 'show'].includes(action) || positional.length > (action === 'use' ? 2 : 1))
|
|
32
|
-
return fail('usage', 'Use project use <saved checkout path> [--token-file PATH], or project
|
|
50
|
+
if (!['use', 'show', 'activity'].includes(action) || positional.length > (action === 'use' ? 2 : 1))
|
|
51
|
+
return fail('usage', 'Use project use <saved checkout path> [--token-file PATH], project show, or project activity [--json].');
|
|
33
52
|
const allowed = new Set(['db', 'json', ...(action === 'use' ? ['token-file'] : [])]);
|
|
34
53
|
for (const name of flags.keys())
|
|
35
54
|
if (!allowed.has(name))
|
|
@@ -57,23 +76,9 @@ export async function runProjectCommand(positional, flags, context) {
|
|
|
57
76
|
if (credential !== undefined && (typeof credential !== 'string' || !credential || /[\x00-\x1f]/.test(credential)))
|
|
58
77
|
return fail('usage', '--token-file requires a path.');
|
|
59
78
|
const tokenFile = typeof credential === 'string' ? resolve(credential) : null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const info = statSync(tokenFile);
|
|
64
|
-
if (!info.isFile() || info.size > 16_384)
|
|
65
|
-
return fail('unauthenticated', 'The scoped credential file is unavailable.');
|
|
66
|
-
token = readFileSync(tokenFile, 'utf8').trim();
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return fail('unauthenticated', 'The scoped credential file is unavailable.');
|
|
70
|
-
}
|
|
71
|
-
const auth = authenticateCoordinator(context.store, token);
|
|
72
|
-
if (!auth.ok)
|
|
73
|
-
return fail('unauthenticated', 'The scoped credential is invalid or revoked.');
|
|
74
|
-
if (!auth.who.repos.includes(repo))
|
|
75
|
-
return fail('not-found', 'That project is unavailable to this credential.');
|
|
76
|
-
}
|
|
79
|
+
const refused = tokenFile === null ? null : scopedCredentialRefusal(context.store, tokenFile, repo);
|
|
80
|
+
if (refused)
|
|
81
|
+
return fail(refused.reason, refused.message);
|
|
77
82
|
profile = { version: 1, repo, tokenFile };
|
|
78
83
|
const file = projectProfilePath(context.databaseFile), temp = `${file}.${randomBytes(6).toString('hex')}.tmp`;
|
|
79
84
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -82,6 +87,18 @@ export async function runProjectCommand(positional, flags, context) {
|
|
|
82
87
|
}
|
|
83
88
|
else
|
|
84
89
|
profile = readProjectProfile(context.databaseFile);
|
|
90
|
+
if (action === 'activity') {
|
|
91
|
+
if (profile === null || !repos.includes(profile.repo))
|
|
92
|
+
return fail('not-found', 'Select a saved project with project use first.');
|
|
93
|
+
const refused = profile.tokenFile === null ? null : scopedCredentialRefusal(context.store, profile.tokenFile, profile.repo);
|
|
94
|
+
if (refused)
|
|
95
|
+
return fail(refused.reason, refused.message);
|
|
96
|
+
// Like assignment brief, an uncredentialed local database read has the
|
|
97
|
+
// local operator's access. A saved scoped credential is always re-proved.
|
|
98
|
+
const result = projectOutcomes(context.store, profile.repo, context.now ?? new Date(), { principal: 'operator', repos: [profile.repo] });
|
|
99
|
+
context.write(context.json ? envelopeJson({ ok: true, command, result }) : JSON.stringify(result, null, 2));
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
85
102
|
const result = { profile, saved: profile !== null && repos.includes(profile.repo) };
|
|
86
103
|
context.write(context.json ? envelopeJson({ ok: true, command, result }) : profile === null ? 'No project selected. Use project use <saved checkout path>.' : `${profile.repo}${result.saved ? '' : ' (no longer saved)'}\n${profile.tokenFile === null ? 'Uses your existing local sign-in for operator actions.' : `Scoped credential reference: ${profile.tokenFile}`}`);
|
|
87
104
|
return 0;
|
package/dist/project-delete.js
CHANGED
|
@@ -86,7 +86,9 @@ export function projectRunning(store, repo, now) {
|
|
|
86
86
|
const chats = count("SELECT COUNT(*) AS n FROM mate_turn WHERE state IN ('queued','running') AND id IN (SELECT value FROM json_each(?))", json(d.turns));
|
|
87
87
|
const steps = count("SELECT COUNT(*) AS n FROM flow_step_run WHERE state = 'running' AND card IN (SELECT value FROM json_each(?))", json(d.cards));
|
|
88
88
|
const calls = count("SELECT COUNT(*) AS n FROM subagent_call WHERE state = 'running' AND subagent IN (SELECT value FROM json_each(?))", json(d.subagents));
|
|
89
|
-
const merges = count(`SELECT COUNT(*) AS n FROM merge_intent WHERE state IN ('claimed','firing') AND publication IN (SELECT id FROM publication WHERE task_ref IN (SELECT value FROM json_each(?)))`, json(d.refs))
|
|
89
|
+
const merges = count(`SELECT COUNT(*) AS n FROM merge_intent WHERE state IN ('claimed','firing') AND publication IN (SELECT id FROM publication WHERE task_ref IN (SELECT value FROM json_each(?)))`, json(d.refs))
|
|
90
|
+
// A person's merge between its last check and GitHub's answer (v145); a row older than 15 minutes is a crash.
|
|
91
|
+
+ count("SELECT COUNT(*) AS n FROM merge_in_flight WHERE at > ? AND task_ref IN (SELECT value FROM json_each(?))", new Date(now.getTime() - 15 * 60_000).toISOString(), json(d.refs));
|
|
90
92
|
const checkouts = count("SELECT COUNT(*) AS n FROM worktree WHERE repo = ? AND runner IS NOT NULL AND released_at IS NULL", repo);
|
|
91
93
|
return [
|
|
92
94
|
...(building > 0 ? [`${plural(building, "task")} ${building === 1 ? "is" : "are"} being built`] : []),
|
|
@@ -267,6 +269,8 @@ function deleteRows(store, repo, d, now) {
|
|
|
267
269
|
del("skill_test", `${IN("task_ref")} OR ${IN("source_run")}`, T, R);
|
|
268
270
|
del("merge_blocker", `${IN("publication")} OR ${IN("task_id")}`, publications, K);
|
|
269
271
|
del("merge_intent", IN("publication"), publications);
|
|
272
|
+
del("merge_in_flight", IN("publication"), publications);
|
|
273
|
+
del("deploy_in_flight", IN("task_ref"), T);
|
|
270
274
|
del("publication", IN("id"), publications);
|
|
271
275
|
del("artifact", IN("id"), A);
|
|
272
276
|
// Tasks and their versions.
|
|
@@ -282,7 +286,7 @@ function deleteRows(store, repo, d, now) {
|
|
|
282
286
|
del("workflow_preview", "repo = ?", repo);
|
|
283
287
|
del("coding_handoff_scope", `id IN (SELECT id FROM coding_handoff WHERE json_extract(payload, '$.repo') = ?)`, repo);
|
|
284
288
|
del("coding_handoff", "json_extract(payload, '$.repo') = ?", repo);
|
|
285
|
-
for (const table of ["task_act", "task_replacement"])
|
|
289
|
+
for (const table of ["task_act", "task_replacement", "task_archive"])
|
|
286
290
|
del(table, IN("task_ref"), T);
|
|
287
291
|
del("task_ref", IN("id"), T);
|
|
288
292
|
del("task_scope", IN("task_id"), K);
|
|
@@ -293,7 +297,7 @@ function deleteRows(store, repo, d, now) {
|
|
|
293
297
|
del("flow_trigger", IN("flow"), F);
|
|
294
298
|
del("flow_unattended_run", `grant_id IN (SELECT id FROM flow_unattended_grant WHERE ${IN("flow")})`, F);
|
|
295
299
|
del("flow_unattended_grant", IN("flow"), F);
|
|
296
|
-
for (const table of ["flow_card_watcher", "flow_comment", "flow_event", "flow_mail", "flow_step_run"])
|
|
300
|
+
for (const table of ["flow_card_watcher", "flow_comment", "flow_event", "flow_mail", "flow_step_run", "flow_step_task"])
|
|
297
301
|
del(table, IN("card"), C);
|
|
298
302
|
del("subagent_question", IN("id"), questions);
|
|
299
303
|
del("subagent_call", `${IN("subagent")} OR ${IN("card")}`, M, C);
|