@skrr-ai/cli 0.1.9 → 0.1.11
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/dist/base-command.d.ts +1 -13
- package/dist/base-command.js +78 -1
- package/dist/commands/browser/skill/show.js +7 -1
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +9 -1
- package/dist/commands/commitments/analytics/index.js +2 -0
- package/dist/commands/daemon/byok.d.ts +1 -0
- package/dist/commands/daemon/byok.js +2 -1
- package/dist/commands/daemon/install.d.ts +1 -0
- package/dist/commands/daemon/install.js +2 -1
- package/dist/commands/daemon/login.d.ts +31 -0
- package/dist/commands/daemon/login.js +56 -0
- package/dist/commands/daemon/restart.d.ts +9 -0
- package/dist/commands/daemon/restart.js +36 -0
- package/dist/commands/daemon/start.d.ts +1 -0
- package/dist/commands/daemon/start.js +2 -1
- package/dist/commands/daemon/status.d.ts +1 -0
- package/dist/commands/daemon/status.js +2 -1
- package/dist/commands/daemon/stop.d.ts +1 -0
- package/dist/commands/daemon/stop.js +2 -1
- package/dist/commands/daemon/uninstall.d.ts +1 -0
- package/dist/commands/daemon/uninstall.js +2 -1
- package/dist/commands/daemon/unlock.d.ts +9 -0
- package/dist/commands/daemon/unlock.js +33 -0
- package/dist/commands/goals/key-results/create.js +32 -1
- package/dist/commands/goals/key-results/update.d.ts +11 -0
- package/dist/commands/goals/key-results/update.js +80 -2
- package/dist/commands/goals/plan-now.d.ts +54 -2
- package/dist/commands/goals/plan-now.js +175 -18
- package/dist/commands/goals/planner-config.d.ts +60 -9
- package/dist/commands/goals/planner-config.js +82 -34
- package/dist/commands/goals/revisions.js +17 -0
- package/dist/commands/goals/show.d.ts +17 -0
- package/dist/commands/goals/show.js +90 -3
- package/dist/commands/login.js +36 -3
- package/dist/commands/spaces/create.js +2 -1
- package/dist/commands/spaces/index.js +9 -1
- package/dist/commands/spaces/list.d.ts +18 -0
- package/dist/commands/spaces/list.js +57 -7
- package/dist/commands/spaces/show.js +4 -1
- package/dist/commands/spaces/summary.d.ts +4 -0
- package/dist/commands/spaces/summary.js +77 -1
- package/dist/commands/spaces/update.d.ts +4 -0
- package/dist/commands/spaces/update.js +38 -1
- package/dist/commands/tasks/actionability.js +40 -1
- package/dist/commands/tasks/activity.d.ts +29 -0
- package/dist/commands/tasks/activity.js +47 -0
- package/dist/commands/tasks/complete.d.ts +47 -0
- package/dist/commands/tasks/complete.js +159 -12
- package/dist/commands/tasks/create.d.ts +26 -0
- package/dist/commands/tasks/create.js +60 -1
- package/dist/commands/tasks/events/append.d.ts +2 -0
- package/dist/commands/tasks/events/append.js +41 -10
- package/dist/commands/tasks/events/list.js +60 -9
- package/dist/commands/tasks/output.js +22 -2
- package/dist/commands/tasks/ready.d.ts +38 -0
- package/dist/commands/tasks/ready.js +37 -0
- package/dist/commands/tasks/runs.d.ts +22 -0
- package/dist/commands/tasks/runs.js +110 -2
- package/dist/commands/tasks/show.d.ts +28 -0
- package/dist/commands/tasks/show.js +61 -0
- package/dist/commands/tasks/timeline.d.ts +7 -0
- package/dist/commands/tasks/timeline.js +34 -3
- package/dist/commands/tasks/update.d.ts +20 -0
- package/dist/commands/tasks/update.js +38 -1
- package/dist/commands/whoami.d.ts +40 -0
- package/dist/commands/whoami.js +61 -10
- package/dist/commands/wiki/ls.d.ts +23 -0
- package/dist/commands/wiki/ls.js +63 -10
- package/dist/commands/wiki/mv.d.ts +70 -0
- package/dist/commands/wiki/mv.js +198 -4
- package/dist/commands/wiki/rm.js +12 -1
- package/dist/commands/wiki/write.js +35 -3
- package/dist/help.d.ts +27 -0
- package/dist/help.js +48 -0
- package/dist/hooks/command-not-found.d.ts +31 -0
- package/dist/hooks/command-not-found.js +12 -80
- package/dist/lib/command-miss.d.ts +60 -0
- package/dist/lib/command-miss.js +128 -0
- package/dist/lib/commitment-analytics.d.ts +10 -0
- package/dist/lib/commitment-analytics.js +10 -0
- package/dist/lib/config.d.ts +40 -0
- package/dist/lib/config.js +72 -9
- package/dist/lib/daemonBroker.d.ts +7 -1
- package/dist/lib/daemonBroker.js +71 -0
- package/dist/lib/daemonHandoff.d.ts +38 -0
- package/dist/lib/daemonHandoff.js +297 -0
- package/dist/lib/exec-oversky.d.ts +30 -0
- package/dist/lib/exec-oversky.js +41 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +27 -5
- package/dist/lib/login.js +22 -2
- package/dist/lib/sky-code.js +9 -4
- package/dist/lib/task-transcript.d.ts +9 -0
- package/dist/lib/task-transcript.js +13 -2
- package/dist/lib/tasks.d.ts +34 -0
- package/dist/lib/tasks.js +70 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
- package/oclif.manifest.json +15713 -15480
- package/package.json +5 -3
|
@@ -56,8 +56,47 @@ class TasksActionability extends base_command_1.BaseCommand {
|
|
|
56
56
|
this.log(`Eligible: ${a.actorRefs.map((ref) => `${ref.kind}:${ref.id}`).join(', ')}`);
|
|
57
57
|
}
|
|
58
58
|
if (a.allowedActions.length) {
|
|
59
|
-
|
|
59
|
+
// These are SERVER action names, not commands, and printing them bare as
|
|
60
|
+
// `Actions: retry_task, resolve_task_hold` reads as a list of things to
|
|
61
|
+
// run — so a reader tries `skrr resolve_task_hold` and finds nothing.
|
|
62
|
+
// That gap is what OSK-3815 fixed for one of them by adding `tasks
|
|
63
|
+
// retry`; naming the verb beside each is the rest of it.
|
|
64
|
+
//
|
|
65
|
+
// Unmapped actions print bare on purpose. Inventing a plausible verb for
|
|
66
|
+
// an action whose resolution is genuinely a review conversation is worse
|
|
67
|
+
// than saying nothing — the reader would chase a command that does not
|
|
68
|
+
// do what the name suggests.
|
|
69
|
+
const width = Math.max(...a.allowedActions.map((action) => action.action.length));
|
|
70
|
+
this.log('Actions:');
|
|
71
|
+
for (const { action } of a.allowedActions) {
|
|
72
|
+
const verb = ACTION_VERBS[action];
|
|
73
|
+
this.log(` ${action.padEnd(width)}${verb ? ` → ${verb}` : ''}`);
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
exports.default = TasksActionability;
|
|
79
|
+
/**
|
|
80
|
+
* Server action name → the `skrr` command that performs it.
|
|
81
|
+
*
|
|
82
|
+
* Only mappings verified against the command surface are listed. Four actions
|
|
83
|
+
* are deliberately absent: `request_changes` and `inspect_workflow` resolve
|
|
84
|
+
* through a review conversation rather than one verb, and `inspect_dependency`
|
|
85
|
+
* / `inspect_workflow` are readings rather than actions — the blocker they
|
|
86
|
+
* point at is already printed above as the reason.
|
|
87
|
+
*/
|
|
88
|
+
const ACTION_VERBS = {
|
|
89
|
+
answer_human_input: 'skrr tasks input answer <id>',
|
|
90
|
+
assign_task: 'skrr tasks assign <id> --agent <agent-id>',
|
|
91
|
+
complete_task: 'skrr tasks complete <id>',
|
|
92
|
+
dismiss_human_input: 'skrr tasks input dismiss <id>',
|
|
93
|
+
interrupt_task: 'skrr tasks interrupt <id>',
|
|
94
|
+
request_approval: 'skrr tasks approvals create <id>',
|
|
95
|
+
resolve_approval: 'skrr tasks approvals resolve <id>',
|
|
96
|
+
resolve_task_hold: 'skrr tasks move <id> --status <status>',
|
|
97
|
+
retry_task: 'skrr tasks retry <id>',
|
|
98
|
+
set_execution_mode: 'skrr tasks update <id> --execution-mode <agent|human>',
|
|
99
|
+
start_task: 'skrr tasks start <id>',
|
|
100
|
+
triage_task: 'skrr tasks triage <id>',
|
|
101
|
+
unschedule_task: 'skrr tasks unschedule <id>',
|
|
102
|
+
};
|
|
@@ -17,6 +17,35 @@ export type ActivityRow = {
|
|
|
17
17
|
details: string;
|
|
18
18
|
};
|
|
19
19
|
export declare function activityRow(entry: ActivityEntry): ActivityRow;
|
|
20
|
+
export type RunFailure = {
|
|
21
|
+
runId: string;
|
|
22
|
+
message: string;
|
|
23
|
+
code: string | null;
|
|
24
|
+
causeClass: string | null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The failure reasons the activity log carries, indexed by run. (S3 dogfood finding)
|
|
28
|
+
*
|
|
29
|
+
* `tasks runs` shows `errors: 1` beside a `failed` run and no message, because
|
|
30
|
+
* the run projection does not carry one — that gap is server-side and this
|
|
31
|
+
* cannot close it. What the activity log DOES carry is the full string, on the
|
|
32
|
+
* `execution_failed` entry, with the `runId` right beside it:
|
|
33
|
+
*
|
|
34
|
+
* { action: 'execution_failed',
|
|
35
|
+
* details: { runId, error, code: 'MCP_CONNECTION_UNHEALTHY', causeClass: 'agent_error' } }
|
|
36
|
+
*
|
|
37
|
+
* so the join is exact rather than a guess. Reading it is the honest half of
|
|
38
|
+
* the fix: the reason is surfaced where it is genuinely known, and never
|
|
39
|
+
* synthesised where it is not.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately keyed ONLY on an explicit `details.runId`. An entry without one
|
|
42
|
+
* is skipped rather than attributed to the newest run — a failure pinned to the
|
|
43
|
+
* wrong run reads exactly like a true one, and is worse than none.
|
|
44
|
+
*
|
|
45
|
+
* Last entry wins per run: activity is append-ordered, so a re-finalization
|
|
46
|
+
* that recorded a better reason should not be shadowed by the first attempt.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runFailuresFromActivity(entries: unknown): Map<string, RunFailure>;
|
|
20
49
|
export default class TasksActivity extends BaseCommand {
|
|
21
50
|
static description: string;
|
|
22
51
|
static examples: string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.activityRow = activityRow;
|
|
4
|
+
exports.runFailuresFromActivity = runFailuresFromActivity;
|
|
4
5
|
const core_1 = require("@oclif/core");
|
|
5
6
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
7
|
const base_command_1 = require("../../base-command");
|
|
@@ -13,6 +14,52 @@ function activityRow(entry) {
|
|
|
13
14
|
details: (0, format_1.formatCell)(entry.details ?? entry.metadata),
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The failure reasons the activity log carries, indexed by run. (S3 dogfood finding)
|
|
19
|
+
*
|
|
20
|
+
* `tasks runs` shows `errors: 1` beside a `failed` run and no message, because
|
|
21
|
+
* the run projection does not carry one — that gap is server-side and this
|
|
22
|
+
* cannot close it. What the activity log DOES carry is the full string, on the
|
|
23
|
+
* `execution_failed` entry, with the `runId` right beside it:
|
|
24
|
+
*
|
|
25
|
+
* { action: 'execution_failed',
|
|
26
|
+
* details: { runId, error, code: 'MCP_CONNECTION_UNHEALTHY', causeClass: 'agent_error' } }
|
|
27
|
+
*
|
|
28
|
+
* so the join is exact rather than a guess. Reading it is the honest half of
|
|
29
|
+
* the fix: the reason is surfaced where it is genuinely known, and never
|
|
30
|
+
* synthesised where it is not.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately keyed ONLY on an explicit `details.runId`. An entry without one
|
|
33
|
+
* is skipped rather than attributed to the newest run — a failure pinned to the
|
|
34
|
+
* wrong run reads exactly like a true one, and is worse than none.
|
|
35
|
+
*
|
|
36
|
+
* Last entry wins per run: activity is append-ordered, so a re-finalization
|
|
37
|
+
* that recorded a better reason should not be shadowed by the first attempt.
|
|
38
|
+
*/
|
|
39
|
+
function runFailuresFromActivity(entries) {
|
|
40
|
+
const out = new Map();
|
|
41
|
+
if (!Array.isArray(entries))
|
|
42
|
+
return out;
|
|
43
|
+
for (const raw of entries) {
|
|
44
|
+
const entry = raw;
|
|
45
|
+
if (!entry || typeof entry !== 'object')
|
|
46
|
+
continue;
|
|
47
|
+
const details = entry.details;
|
|
48
|
+
if (!details || typeof details !== 'object')
|
|
49
|
+
continue;
|
|
50
|
+
const runId = typeof details.runId === 'string' ? details.runId.trim() : '';
|
|
51
|
+
const message = typeof details.error === 'string' ? details.error.trim() : '';
|
|
52
|
+
if (!runId || !message)
|
|
53
|
+
continue;
|
|
54
|
+
out.set(runId, {
|
|
55
|
+
runId,
|
|
56
|
+
message,
|
|
57
|
+
code: typeof details.code === 'string' ? details.code : null,
|
|
58
|
+
causeClass: typeof details.causeClass === 'string' ? details.causeClass : null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
16
63
|
/** Same contract as `tasks monitor`'s timestamp column — never throws, never prints "undefined". */
|
|
17
64
|
function formatTime(value) {
|
|
18
65
|
if (!value)
|
|
@@ -41,6 +41,52 @@ export interface DurableCompletionResult {
|
|
|
41
41
|
* old server and fall through to the legacy comment-first path.
|
|
42
42
|
*/
|
|
43
43
|
export declare function resolveCanonicalCompletionTaskId(taskRef: string, lookup: (taskRef: string) => Promise<unknown>): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* The canonical id AND the status it is in.
|
|
46
|
+
*
|
|
47
|
+
* The status comes free: this lookup already happens on the user path, so the
|
|
48
|
+
* re-completion guard below costs no extra round trip on the common case. It is
|
|
49
|
+
* also the only way to make the success line honest — `tasks complete` printed
|
|
50
|
+
* "Moved task X to done" unconditionally, so a task that was ALREADY done got a
|
|
51
|
+
* report of a state transition that did not happen.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveCompletionTarget(taskRef: string, lookup: (taskRef: string) => Promise<unknown>): Promise<{
|
|
54
|
+
id: string;
|
|
55
|
+
status: string | null;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* What a second `tasks complete` on an already-closed task should do.
|
|
59
|
+
*
|
|
60
|
+
* The repo contract calls this "the only terminal closeout", and a task has one
|
|
61
|
+
* canonical outcome — but a second run used to succeed silently, post another
|
|
62
|
+
* TASK_OUTPUT block, and leave `tasks output` rendering the throwaway one as
|
|
63
|
+
* authoritative. The real summary and its artifacts link were still on the task
|
|
64
|
+
* but no longer canonical, which is the worst of the available failures: no
|
|
65
|
+
* error, no warning, exit 0, and a record that now says something different.
|
|
66
|
+
*
|
|
67
|
+
* Refusing outright would break the case that makes retries safe. A completion
|
|
68
|
+
* whose response is lost — a flaky connection, a re-run script — SHOULD be able
|
|
69
|
+
* to run again, and today it collapses server-side on the shared
|
|
70
|
+
* `X-Idempotency-Key`. Turning that into a hard failure would force every
|
|
71
|
+
* caller to write its own is-it-already-done check.
|
|
72
|
+
*
|
|
73
|
+
* So the axis is not "again or not", it is "the SAME again, or something
|
|
74
|
+
* different":
|
|
75
|
+
*
|
|
76
|
+
* - identical report → `noop`, exit 0. Nothing to do, and saying so is more
|
|
77
|
+
* honest than re-posting an identical block.
|
|
78
|
+
* - differing report → `refuse`. Superseding a canonical outcome is a real
|
|
79
|
+
* decision, so it takes `--amend` and says so.
|
|
80
|
+
* - not yet terminal → `proceed`, unchanged.
|
|
81
|
+
*
|
|
82
|
+
* Pure, so the rule is testable without a server.
|
|
83
|
+
*/
|
|
84
|
+
export declare function decideRecompletion(opts: {
|
|
85
|
+
priorStatus: string | null;
|
|
86
|
+
canonicalBody: string | null;
|
|
87
|
+
incomingBody: string;
|
|
88
|
+
amend: boolean;
|
|
89
|
+
}): 'proceed' | 'noop' | 'refuse';
|
|
44
90
|
/** A 404 from the /complete endpoint means an old server without the route. */
|
|
45
91
|
export declare function isCompleteEndpointMissing(err: unknown): boolean;
|
|
46
92
|
/**
|
|
@@ -90,6 +136,7 @@ export default class TasksComplete extends BaseCommand {
|
|
|
90
136
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
91
137
|
task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
92
138
|
'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
139
|
+
amend: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
93
140
|
status: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
94
141
|
summary: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
95
142
|
artifacts: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskCompletionMismatchError = void 0;
|
|
4
4
|
exports.resolveCanonicalCompletionTaskId = resolveCanonicalCompletionTaskId;
|
|
5
|
+
exports.resolveCompletionTarget = resolveCompletionTarget;
|
|
6
|
+
exports.decideRecompletion = decideRecompletion;
|
|
5
7
|
exports.isCompleteEndpointMissing = isCompleteEndpointMissing;
|
|
6
8
|
exports.completeTaskDurably = completeTaskDurably;
|
|
7
9
|
const core_1 = require("@oclif/core");
|
|
@@ -16,7 +18,9 @@ const prompt_1 = require("../../lib/prompt");
|
|
|
16
18
|
const task_report_1 = require("../../lib/task-report");
|
|
17
19
|
const task_resolver_1 = require("../../lib/task-resolver");
|
|
18
20
|
const task_instruction_offer_1 = require("../../lib/task-instruction-offer");
|
|
21
|
+
const task_transcript_1 = require("../../lib/task-transcript");
|
|
19
22
|
const web_url_1 = require("../../lib/web-url");
|
|
23
|
+
const tasks_1 = require("../../lib/tasks");
|
|
20
24
|
/**
|
|
21
25
|
* Mutation endpoints require the task's canonical UUID even though the CLI
|
|
22
26
|
* accepts human-readable refs and titles. Resolve the display reference before
|
|
@@ -24,12 +28,65 @@ const web_url_1 = require("../../lib/web-url");
|
|
|
24
28
|
* old server and fall through to the legacy comment-first path.
|
|
25
29
|
*/
|
|
26
30
|
async function resolveCanonicalCompletionTaskId(taskRef, lookup) {
|
|
31
|
+
return (await resolveCompletionTarget(taskRef, lookup)).id;
|
|
32
|
+
}
|
|
33
|
+
/** Terminal workflow states — a task in one of these has already been closed out. */
|
|
34
|
+
const TERMINAL_STATUSES = new Set(['done', 'failed']);
|
|
35
|
+
/**
|
|
36
|
+
* The canonical id AND the status it is in.
|
|
37
|
+
*
|
|
38
|
+
* The status comes free: this lookup already happens on the user path, so the
|
|
39
|
+
* re-completion guard below costs no extra round trip on the common case. It is
|
|
40
|
+
* also the only way to make the success line honest — `tasks complete` printed
|
|
41
|
+
* "Moved task X to done" unconditionally, so a task that was ALREADY done got a
|
|
42
|
+
* report of a state transition that did not happen.
|
|
43
|
+
*/
|
|
44
|
+
async function resolveCompletionTarget(taskRef, lookup) {
|
|
27
45
|
const task = (await lookup(taskRef));
|
|
28
46
|
const id = typeof task?.id === 'string' ? task.id.trim() : '';
|
|
29
47
|
if (!id) {
|
|
30
48
|
throw new Error(`Task ${taskRef} did not resolve to a canonical id.`);
|
|
31
49
|
}
|
|
32
|
-
return id;
|
|
50
|
+
return { id, status: typeof task?.status === 'string' ? task.status : null };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What a second `tasks complete` on an already-closed task should do.
|
|
54
|
+
*
|
|
55
|
+
* The repo contract calls this "the only terminal closeout", and a task has one
|
|
56
|
+
* canonical outcome — but a second run used to succeed silently, post another
|
|
57
|
+
* TASK_OUTPUT block, and leave `tasks output` rendering the throwaway one as
|
|
58
|
+
* authoritative. The real summary and its artifacts link were still on the task
|
|
59
|
+
* but no longer canonical, which is the worst of the available failures: no
|
|
60
|
+
* error, no warning, exit 0, and a record that now says something different.
|
|
61
|
+
*
|
|
62
|
+
* Refusing outright would break the case that makes retries safe. A completion
|
|
63
|
+
* whose response is lost — a flaky connection, a re-run script — SHOULD be able
|
|
64
|
+
* to run again, and today it collapses server-side on the shared
|
|
65
|
+
* `X-Idempotency-Key`. Turning that into a hard failure would force every
|
|
66
|
+
* caller to write its own is-it-already-done check.
|
|
67
|
+
*
|
|
68
|
+
* So the axis is not "again or not", it is "the SAME again, or something
|
|
69
|
+
* different":
|
|
70
|
+
*
|
|
71
|
+
* - identical report → `noop`, exit 0. Nothing to do, and saying so is more
|
|
72
|
+
* honest than re-posting an identical block.
|
|
73
|
+
* - differing report → `refuse`. Superseding a canonical outcome is a real
|
|
74
|
+
* decision, so it takes `--amend` and says so.
|
|
75
|
+
* - not yet terminal → `proceed`, unchanged.
|
|
76
|
+
*
|
|
77
|
+
* Pure, so the rule is testable without a server.
|
|
78
|
+
*/
|
|
79
|
+
function decideRecompletion(opts) {
|
|
80
|
+
const { priorStatus, canonicalBody, incomingBody, amend } = opts;
|
|
81
|
+
if (!priorStatus || !TERMINAL_STATUSES.has(priorStatus))
|
|
82
|
+
return 'proceed';
|
|
83
|
+
if (amend)
|
|
84
|
+
return 'proceed';
|
|
85
|
+
// No canonical block to disagree with — a task closed by a path that skipped
|
|
86
|
+
// it, so writing one is an addition rather than a replacement.
|
|
87
|
+
if (canonicalBody === null)
|
|
88
|
+
return 'proceed';
|
|
89
|
+
return canonicalBody.trim() === incomingBody.trim() ? 'noop' : 'refuse';
|
|
33
90
|
}
|
|
34
91
|
/** A 404 from the /complete endpoint means an old server without the route. */
|
|
35
92
|
function isCompleteEndpointMissing(err) {
|
|
@@ -144,6 +201,9 @@ class TasksComplete extends base_command_1.BaseCommand {
|
|
|
144
201
|
'from-json': core_1.Flags.string({
|
|
145
202
|
description: 'Path to JSON with taskId, status, summary, artifacts, and verdict; "-" for stdin',
|
|
146
203
|
}),
|
|
204
|
+
amend: core_1.Flags.boolean({
|
|
205
|
+
description: 'Supersede the canonical outcome of an already-completed task. Without it, a differing report on a closed task is refused',
|
|
206
|
+
}),
|
|
147
207
|
status: core_1.Flags.string({
|
|
148
208
|
description: 'TASK_OUTPUT status',
|
|
149
209
|
options: ['completed', 'failed'],
|
|
@@ -221,6 +281,16 @@ class TasksComplete extends base_command_1.BaseCommand {
|
|
|
221
281
|
]) || 'none',
|
|
222
282
|
verdict: flags.verdict,
|
|
223
283
|
});
|
|
284
|
+
// `--verdict` is deliberately not enumerated — a reviewer task passes a
|
|
285
|
+
// JSON posture object, and `normalizeReviewPosture` accepts synonyms. But
|
|
286
|
+
// when it recognises nothing it derives the posture from the criteria
|
|
287
|
+
// rather than failing, so a typo is recorded and silently means something
|
|
288
|
+
// else. Warn over the set the server actually matches; never refuse.
|
|
289
|
+
if (!(0, tasks_1.verdictIsRecognized)(input.verdict)) {
|
|
290
|
+
this.warn(`Verdict "${input.verdict}" is not one the server recognises ` +
|
|
291
|
+
`(${tasks_1.RECOGNIZED_TASK_VERDICTS.join(', ')}, or a JSON review posture). ` +
|
|
292
|
+
'It will be recorded as written, and a review will fall back to what the criteria imply.');
|
|
293
|
+
}
|
|
224
294
|
let taskIdentity;
|
|
225
295
|
try {
|
|
226
296
|
taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({
|
|
@@ -316,18 +386,88 @@ class TasksComplete extends base_command_1.BaseCommand {
|
|
|
316
386
|
noMove: !!flags['no-move'],
|
|
317
387
|
};
|
|
318
388
|
const completionIdempotencyKey = (0, outbox_1.idempotencyKeyFor)('task-complete', taskIdentity.id, JSON.stringify(completionPayload));
|
|
389
|
+
const sessionCompletionPath = autonomousSessionCompletionPath();
|
|
390
|
+
// Resolved OUTSIDE the write's try/catch. It is a GET, so a failure here is
|
|
391
|
+
// not a queueable write — and the re-completion guard has to be able to
|
|
392
|
+
// refuse without `queueWriteOrFail` treating the refusal as an outage worth
|
|
393
|
+
// replaying later.
|
|
394
|
+
//
|
|
395
|
+
// The session endpoint derives its task from the trusted run claim and does
|
|
396
|
+
// not need (or permit) a broad task lookup. The normal user and
|
|
397
|
+
// delegated-daemon path does: task routes read OSK refs, while mutation
|
|
398
|
+
// services key on UUIDs. Passing the ref straight through caused the atomic
|
|
399
|
+
// endpoint's task-level 404 to look like "endpoint missing"; legacy fallback
|
|
400
|
+
// then posted TASK_OUTPUT before its move 404'd.
|
|
401
|
+
let completionTaskId = taskIdentity.id;
|
|
402
|
+
// Captured at resolution so the guard and the success line can both tell
|
|
403
|
+
// "already closed" from "just closed".
|
|
404
|
+
let priorStatus = null;
|
|
405
|
+
if (!sessionCompletionPath) {
|
|
406
|
+
try {
|
|
407
|
+
const target = await resolveCompletionTarget(await this.resolveTaskReference(taskIdentity.id), (taskRef) => data_provider_1.dataService.getTask(taskRef, { enrich: false }));
|
|
408
|
+
completionTaskId = target.id;
|
|
409
|
+
priorStatus = target.status;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
this.handleApiError(err);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// A task that is already closed gets one extra read, and only then — the
|
|
416
|
+
// first completion of a task never pays for this.
|
|
417
|
+
if (decideRecompletion({
|
|
418
|
+
priorStatus,
|
|
419
|
+
canonicalBody: '',
|
|
420
|
+
incomingBody: body,
|
|
421
|
+
amend: flags.amend,
|
|
422
|
+
}) !== 'proceed') {
|
|
423
|
+
let canonicalBody = null;
|
|
424
|
+
try {
|
|
425
|
+
canonicalBody = (await (0, task_transcript_1.fetchLatestTaskOutputComment)(completionTaskId))?.body ?? null;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// Unreadable history must not block a completion: falling through to
|
|
429
|
+
// `proceed` is exactly today's behaviour, which is the safe direction
|
|
430
|
+
// for a guard whose whole job is to protect a record it cannot see.
|
|
431
|
+
canonicalBody = null;
|
|
432
|
+
}
|
|
433
|
+
const decision = decideRecompletion({
|
|
434
|
+
priorStatus,
|
|
435
|
+
canonicalBody,
|
|
436
|
+
incomingBody: body,
|
|
437
|
+
amend: flags.amend,
|
|
438
|
+
});
|
|
439
|
+
if (decision === 'noop') {
|
|
440
|
+
const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, { id: completionTaskId });
|
|
441
|
+
if (flags.json) {
|
|
442
|
+
this.log(JSON.stringify({
|
|
443
|
+
taskId: completionTaskId,
|
|
444
|
+
status: priorStatus,
|
|
445
|
+
alreadyCompleted: true,
|
|
446
|
+
changed: false,
|
|
447
|
+
url: url ? `${url}#task-outcome` : url,
|
|
448
|
+
}, null, 2));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.log(`Already ${priorStatus}; this report is identical to the canonical one — nothing to do.`);
|
|
452
|
+
if (url)
|
|
453
|
+
this.log(`Outcome: ${url}#task-outcome`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (decision === 'refuse') {
|
|
457
|
+
this.failWithCliError({
|
|
458
|
+
message: `Task ${completionTaskId} is already ${priorStatus}, and this report differs from its canonical ` +
|
|
459
|
+
`outcome — completing again would silently replace it (artifacts included).\n` +
|
|
460
|
+
` → re-run with \`--amend\` to supersede it deliberately, or read what is there now with ` +
|
|
461
|
+
`\`${this.config.bin} tasks output ${completionTaskId}\`.`,
|
|
462
|
+
code: 'TASK_ALREADY_COMPLETED',
|
|
463
|
+
exit: 1,
|
|
464
|
+
retryable: false,
|
|
465
|
+
details: { taskId: completionTaskId, status: priorStatus },
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
319
469
|
let result;
|
|
320
470
|
try {
|
|
321
|
-
const sessionCompletionPath = autonomousSessionCompletionPath();
|
|
322
|
-
// The session endpoint derives its task from the trusted run claim and
|
|
323
|
-
// does not need (or permit) a broad task lookup. The normal user and
|
|
324
|
-
// delegated-daemon path does: task routes read OSK refs, while mutation
|
|
325
|
-
// services key on UUIDs. Passing the ref straight through caused the
|
|
326
|
-
// atomic endpoint's task-level 404 to look like "endpoint missing";
|
|
327
|
-
// legacy fallback then posted TASK_OUTPUT before its move 404'd.
|
|
328
|
-
const completionTaskId = sessionCompletionPath
|
|
329
|
-
? taskIdentity.id
|
|
330
|
-
: await resolveCanonicalCompletionTaskId(await this.resolveTaskReference(taskIdentity.id), (taskRef) => data_provider_1.dataService.getTask(taskRef, { enrich: false }));
|
|
331
471
|
result = await completeTaskDurably({
|
|
332
472
|
taskId: completionTaskId,
|
|
333
473
|
status,
|
|
@@ -409,7 +549,14 @@ class TasksComplete extends base_command_1.BaseCommand {
|
|
|
409
549
|
: undefined;
|
|
410
550
|
this.log(`Posted TASK_OUTPUT comment ${commentId ?? '?'} on task ${completedTaskId}`);
|
|
411
551
|
if (movedTask) {
|
|
412
|
-
|
|
552
|
+
// Only claim a transition that happened. This printed unconditionally, so
|
|
553
|
+
// `tasks complete` on an already-done task reported "Moved task X to
|
|
554
|
+
// done" when nothing moved — a state change asserted by the one tool the
|
|
555
|
+
// reader is trusting to tell them the state.
|
|
556
|
+
const newStatus = movedTask.status;
|
|
557
|
+
this.log(priorStatus && priorStatus === newStatus
|
|
558
|
+
? `Task ${completedTaskId} was already ${newStatus}; the outcome was amended.`
|
|
559
|
+
: `Moved task ${completedTaskId} to ${newStatus}`);
|
|
413
560
|
}
|
|
414
561
|
if (report) {
|
|
415
562
|
const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, movedTask ?? { id: completedTaskId });
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { BaseCommand } from '../../base-command';
|
|
2
|
+
/**
|
|
3
|
+
* Whether the estimate we asked for survived the write. (S3 dogfood finding)
|
|
4
|
+
*
|
|
5
|
+
* `--estimate 3` exited 0 with empty stderr and stored `null`. The value is
|
|
6
|
+
* dropped because an estimate is a SPACE-owned vocabulary: `assertTaskEstimateAllowed`
|
|
7
|
+
* resolves the scale up the Space's ancestor chain, and a chain with no
|
|
8
|
+
* `estimateScale` anywhere resolves to the `none` default — which allows no
|
|
9
|
+
* value, so the field is simply not persisted. It does not throw, because
|
|
10
|
+
* "this Space does not track estimates" is a legitimate configuration rather
|
|
11
|
+
* than a bad request.
|
|
12
|
+
*
|
|
13
|
+
* That is defensible server-side. What is not defensible is the CLI sending a
|
|
14
|
+
* value, receiving a response that plainly does not contain it, and reporting
|
|
15
|
+
* success — the caller cannot tell "stored" from "discarded" without a second
|
|
16
|
+
* read they have no reason to make.
|
|
17
|
+
*
|
|
18
|
+
* So this is a POSTCONDITION check on the response we already have: no extra
|
|
19
|
+
* round trip, and exact rather than predicted — it reports what the server
|
|
20
|
+
* actually did, including snapping a value onto a coarser scale. It warns and
|
|
21
|
+
* does not fail: the task WAS created, and exiting non-zero over one dropped
|
|
22
|
+
* field would be a worse lie than the silence it replaces.
|
|
23
|
+
*
|
|
24
|
+
* Same shape as `warnDroppedDefaultAssignees` below, which does this for a
|
|
25
|
+
* silently-dropped assignee (OSK-4857).
|
|
26
|
+
*/
|
|
27
|
+
export declare function droppedEstimateWarning(requested: number | undefined, written: Record<string, unknown> | null | undefined, spaceId?: string | null): string | null;
|
|
2
28
|
export default class TasksCreate extends BaseCommand {
|
|
3
29
|
static description: string;
|
|
4
30
|
static examples: string[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.droppedEstimateWarning = droppedEstimateWarning;
|
|
3
4
|
const core_1 = require("@oclif/core");
|
|
4
5
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
6
|
const base_command_1 = require("../../base-command");
|
|
@@ -7,6 +8,49 @@ const prompt_1 = require("../../lib/prompt");
|
|
|
7
8
|
const tasks_1 = require("../../lib/tasks");
|
|
8
9
|
const task_instruction_offer_1 = require("../../lib/task-instruction-offer");
|
|
9
10
|
const web_url_1 = require("../../lib/web-url");
|
|
11
|
+
/**
|
|
12
|
+
* Whether the estimate we asked for survived the write. (S3 dogfood finding)
|
|
13
|
+
*
|
|
14
|
+
* `--estimate 3` exited 0 with empty stderr and stored `null`. The value is
|
|
15
|
+
* dropped because an estimate is a SPACE-owned vocabulary: `assertTaskEstimateAllowed`
|
|
16
|
+
* resolves the scale up the Space's ancestor chain, and a chain with no
|
|
17
|
+
* `estimateScale` anywhere resolves to the `none` default — which allows no
|
|
18
|
+
* value, so the field is simply not persisted. It does not throw, because
|
|
19
|
+
* "this Space does not track estimates" is a legitimate configuration rather
|
|
20
|
+
* than a bad request.
|
|
21
|
+
*
|
|
22
|
+
* That is defensible server-side. What is not defensible is the CLI sending a
|
|
23
|
+
* value, receiving a response that plainly does not contain it, and reporting
|
|
24
|
+
* success — the caller cannot tell "stored" from "discarded" without a second
|
|
25
|
+
* read they have no reason to make.
|
|
26
|
+
*
|
|
27
|
+
* So this is a POSTCONDITION check on the response we already have: no extra
|
|
28
|
+
* round trip, and exact rather than predicted — it reports what the server
|
|
29
|
+
* actually did, including snapping a value onto a coarser scale. It warns and
|
|
30
|
+
* does not fail: the task WAS created, and exiting non-zero over one dropped
|
|
31
|
+
* field would be a worse lie than the silence it replaces.
|
|
32
|
+
*
|
|
33
|
+
* Same shape as `warnDroppedDefaultAssignees` below, which does this for a
|
|
34
|
+
* silently-dropped assignee (OSK-4857).
|
|
35
|
+
*/
|
|
36
|
+
function droppedEstimateWarning(requested, written, spaceId) {
|
|
37
|
+
if (requested === undefined || requested === null)
|
|
38
|
+
return null;
|
|
39
|
+
if (!written || typeof written !== 'object')
|
|
40
|
+
return null;
|
|
41
|
+
const stored = written.estimate;
|
|
42
|
+
if (stored === requested)
|
|
43
|
+
return null;
|
|
44
|
+
const subject = spaceId ? `space ${spaceId}` : 'this Space';
|
|
45
|
+
if (typeof stored === 'number') {
|
|
46
|
+
return (`--estimate ${requested} was stored as ${stored}: the estimate scale on ${subject} does ` +
|
|
47
|
+
`not allow ${requested}, so the server snapped it to the nearest value it does.`);
|
|
48
|
+
}
|
|
49
|
+
return (`--estimate ${requested} was NOT stored — the task came back with no estimate. ` +
|
|
50
|
+
`Estimates are owned by the Space: ${subject} (and every ancestor it inherits from) has no ` +
|
|
51
|
+
'estimate scale, so the value is discarded rather than rejected. ' +
|
|
52
|
+
'Set one with `spaces update <space> --estimate-scale <scale> --estimate-migration <clear|migrate>`.');
|
|
53
|
+
}
|
|
10
54
|
class TasksCreate extends base_command_1.BaseCommand {
|
|
11
55
|
static description = 'Create a new task';
|
|
12
56
|
static examples = [
|
|
@@ -100,7 +144,17 @@ class TasksCreate extends base_command_1.BaseCommand {
|
|
|
100
144
|
}),
|
|
101
145
|
'start-date': core_1.Flags.string({ description: 'Planned start date (ISO string)' }),
|
|
102
146
|
'due-date': core_1.Flags.string({ description: 'Due date (ISO string)' }),
|
|
103
|
-
estimate: core_1.Flags.integer({
|
|
147
|
+
estimate: core_1.Flags.integer({
|
|
148
|
+
// Names the prerequisite AND how to satisfy it. The first version of this
|
|
149
|
+
// help said the scale was reachable only from the web UI, which was true
|
|
150
|
+
// when written and stopped being true the moment `spaces update` grew the
|
|
151
|
+
// flag — a help string asserting a limitation has to be revisited when the
|
|
152
|
+
// limitation is lifted, or it becomes the most authoritative wrong answer
|
|
153
|
+
// in the product.
|
|
154
|
+
description: 'Estimate value from the Space estimate scale. Requires a scale on the Space (or an ' +
|
|
155
|
+
'ancestor it inherits from); with none set, the server drops the value. Set one with ' +
|
|
156
|
+
'`spaces update <space> --estimate-scale <scale> --estimate-migration <clear|migrate>`.',
|
|
157
|
+
}),
|
|
104
158
|
tag: core_1.Flags.string({ description: 'Tag (repeatable)', multiple: true }),
|
|
105
159
|
'label-id': core_1.Flags.string({
|
|
106
160
|
description: 'Attach a task label by id (repeatable). Manage labels with `skrr tasks labels`. ' +
|
|
@@ -333,6 +387,11 @@ class TasksCreate extends base_command_1.BaseCommand {
|
|
|
333
387
|
}
|
|
334
388
|
}
|
|
335
389
|
const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, created);
|
|
390
|
+
// Before either output path, and via `warn` so it reaches stderr — `--json`
|
|
391
|
+
// stdout stays a parseable task row.
|
|
392
|
+
const estimateWarning = droppedEstimateWarning(flags.estimate, created, payload.spaceId);
|
|
393
|
+
if (estimateWarning)
|
|
394
|
+
this.warn(estimateWarning);
|
|
336
395
|
if (flags.json) {
|
|
337
396
|
// The raw task row alone cannot tell a script the assignee it implicitly
|
|
338
397
|
// asked for was dropped (`assigneeAgentIds: []`) from "nobody was ever
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BaseCommand } from '../../../base-command';
|
|
2
2
|
export default class TasksEventsAppend extends BaseCommand {
|
|
3
|
+
static summary: string;
|
|
3
4
|
static description: string;
|
|
5
|
+
static examples: string[];
|
|
4
6
|
static args: {
|
|
5
7
|
id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
6
8
|
};
|
|
@@ -8,12 +8,38 @@ const api_fetch_1 = require("../../../lib/api-fetch");
|
|
|
8
8
|
const session_task_endpoints_1 = require("../../../lib/session-task-endpoints");
|
|
9
9
|
const tasks_1 = require("../../../lib/tasks");
|
|
10
10
|
class TasksEventsAppend extends base_command_1.BaseCommand {
|
|
11
|
-
static
|
|
12
|
-
static
|
|
11
|
+
static summary = 'Append a typed, durable task event';
|
|
12
|
+
static description = 'Records one meaningful work boundary — a decision, a material change, evidence, a blocker, ' +
|
|
13
|
+
'or a handoff — on the task you are executing. Not a log of tool calls: an event that is ' +
|
|
14
|
+
'worth reading six weeks from now. `skrr tasks timeline <id>` reads them back.';
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> tasks events append <task-id> --kind checkpoint --idempotency-key "<task-id>:first-pass" --summary "Parser handles nested fences"',
|
|
17
|
+
'<%= config.bin %> tasks events append <task-id> --kind decision --idempotency-key "<task-id>:storage-choice" --summary "Chose the CRDT path" --evidence "A direct contentHtml write is reverted by the next render tick"',
|
|
18
|
+
'<%= config.bin %> tasks events append <task-id> --kind verification --idempotency-key "<task-id>:verified" --summary "Repro no longer reproduces" --artifact <commit-sha>',
|
|
19
|
+
];
|
|
20
|
+
static args = {
|
|
21
|
+
id: core_1.Args.string({
|
|
22
|
+
required: false,
|
|
23
|
+
ignoreStdin: true,
|
|
24
|
+
description: 'Task ID. Optional inside a task runtime, which supplies it',
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
13
27
|
static flags = {
|
|
14
|
-
task: core_1.Flags.string(
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
task: core_1.Flags.string({
|
|
29
|
+
description: 'Task ID; overrides the runtime context and the positional',
|
|
30
|
+
}),
|
|
31
|
+
json: core_1.Flags.boolean({ description: 'Output the created event as JSON' }),
|
|
32
|
+
// The semantics here are the ones that actually bite, and they lived only
|
|
33
|
+
// in the platform's instruction block: a key names ONE update permanently.
|
|
34
|
+
// Retried with the same text the server silently returns the original; with
|
|
35
|
+
// DIFFERENT text it refuses with 409 TASK_EVENT_IDEMPOTENCY_CONFLICT. A
|
|
36
|
+
// caller who does not know that reads a dedupe as a successful write.
|
|
37
|
+
'idempotency-key': core_1.Flags.string({
|
|
38
|
+
required: true,
|
|
39
|
+
description: 'Stable key naming THIS update permanently, e.g. "<task-id>:verified". Reuse it only to ' +
|
|
40
|
+
'retry the same update — reused with identical text the original is returned, with ' +
|
|
41
|
+
'different text the server refuses (409 TASK_EVENT_IDEMPOTENCY_CONFLICT)',
|
|
42
|
+
}),
|
|
17
43
|
// Enumerated so `--help` lists the kinds and a bad value is rejected by the
|
|
18
44
|
// flag parser (naming the choices) rather than by the server's opaque
|
|
19
45
|
// `Invalid request data` / HTTP_400. (OSK-4888)
|
|
@@ -22,11 +48,16 @@ class TasksEventsAppend extends base_command_1.BaseCommand {
|
|
|
22
48
|
description: 'Event kind at a meaningful work boundary',
|
|
23
49
|
options: [...tasks_1.TASK_WORK_EVENT_KINDS],
|
|
24
50
|
}),
|
|
25
|
-
summary: core_1.Flags.string({ required: true }),
|
|
26
|
-
changed: core_1.Flags.string(),
|
|
27
|
-
next: core_1.Flags.string(),
|
|
28
|
-
evidence: core_1.Flags.string(
|
|
29
|
-
|
|
51
|
+
summary: core_1.Flags.string({ required: true, description: 'What is newly true, in one sentence' }),
|
|
52
|
+
changed: core_1.Flags.string({ description: 'What moved — the concrete change this event records' }),
|
|
53
|
+
next: core_1.Flags.string({ description: 'What happens next, or who owns the next decision' }),
|
|
54
|
+
evidence: core_1.Flags.string({
|
|
55
|
+
description: 'Why it is true — an observation, command output, or a before → after',
|
|
56
|
+
}),
|
|
57
|
+
artifact: core_1.Flags.string({
|
|
58
|
+
multiple: true,
|
|
59
|
+
description: 'Durable reference: a file path, commit sha, or URL. Repeatable. "in the conversation" is not an artifact',
|
|
60
|
+
}),
|
|
30
61
|
};
|
|
31
62
|
async run() {
|
|
32
63
|
this.requireAuth();
|