@skrr-ai/cli 0.1.23 → 0.1.25
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/bin/run.js +54 -1
- package/dist/base-command.d.ts +4 -4
- package/dist/base-command.js +81 -6
- package/dist/commands/harnesses/leases/list.js +1 -1
- package/dist/commands/harnesses/products.js +1 -0
- package/dist/commands/machines/dedicated/create.d.ts +18 -0
- package/dist/commands/machines/dedicated/create.js +90 -0
- package/dist/commands/machines/dedicated/destroy.d.ts +14 -0
- package/dist/commands/machines/dedicated/destroy.js +56 -0
- package/dist/commands/machines/dedicated/grow.d.ts +13 -0
- package/dist/commands/machines/dedicated/grow.js +46 -0
- package/dist/commands/machines/dedicated/health-check.d.ts +12 -0
- package/dist/commands/machines/dedicated/health-check.js +42 -0
- package/dist/commands/machines/dedicated/incident-close.d.ts +12 -0
- package/dist/commands/machines/dedicated/incident-close.js +42 -0
- package/dist/commands/machines/dedicated/index.d.ts +6 -0
- package/dist/commands/machines/dedicated/index.js +39 -0
- package/dist/commands/machines/dedicated/list.d.ts +11 -0
- package/dist/commands/machines/dedicated/list.js +58 -0
- package/dist/commands/machines/dedicated/restart.d.ts +12 -0
- package/dist/commands/machines/dedicated/restart.js +42 -0
- package/dist/commands/machines/dedicated/show.d.ts +12 -0
- package/dist/commands/machines/dedicated/show.js +58 -0
- package/dist/commands/machines/dedicated/snapshot.d.ts +12 -0
- package/dist/commands/machines/dedicated/snapshot.js +42 -0
- package/dist/commands/machines/dedicated/start.d.ts +12 -0
- package/dist/commands/machines/dedicated/start.js +42 -0
- package/dist/commands/machines/dedicated/stop.d.ts +12 -0
- package/dist/commands/machines/dedicated/stop.js +42 -0
- package/dist/commands/machines/hosted/index.js +4 -1
- package/dist/commands/spaces/auto-executor/preview.d.ts +8 -0
- package/dist/commands/spaces/auto-executor/preview.js +41 -0
- package/dist/commands/spaces/task-rollups.d.ts +14 -0
- package/dist/commands/spaces/task-rollups.js +66 -0
- package/dist/commands/spaces/update.js +16 -12
- package/dist/commands/spaces/work-sync/reconcile.d.ts +38 -0
- package/dist/commands/spaces/work-sync/reconcile.js +90 -0
- package/dist/commands/spaces/work-sync/telemetry.d.ts +29 -0
- package/dist/commands/spaces/work-sync/telemetry.js +118 -0
- package/dist/commands/tasks/activity.js +5 -1
- package/dist/commands/tasks/assign.js +1 -1
- package/dist/commands/tasks/complete.d.ts +5 -1
- package/dist/commands/tasks/complete.js +17 -3
- package/dist/commands/tasks/create.d.ts +1 -0
- package/dist/commands/tasks/create.js +17 -4
- package/dist/commands/tasks/deliverable/add.js +22 -4
- package/dist/commands/tasks/deliverable/list.js +23 -2
- package/dist/commands/tasks/events/append.js +31 -3
- package/dist/commands/tasks/expectations/assess.d.ts +1 -1
- package/dist/commands/tasks/expectations/assess.js +33 -7
- package/dist/commands/tasks/expectations.js +110 -10
- package/dist/commands/tasks/output.d.ts +0 -1
- package/dist/commands/tasks/output.js +21 -1
- package/dist/commands/tasks/report.js +3 -1
- package/dist/commands/tasks/result/show.js +20 -2
- package/dist/commands/tasks/resume/save.js +11 -1
- package/dist/commands/tasks/self-schedule.js +7 -4
- package/dist/commands/tasks/show.js +19 -0
- package/dist/commands/tasks/timeline.js +9 -1
- package/dist/commands/tasks/updates/add.js +23 -2
- package/dist/lib/api-fetch.js +9 -1
- package/dist/lib/assignee-resolver.d.ts +7 -6
- package/dist/lib/daemonBroker.d.ts +20 -0
- package/dist/lib/daemonBroker.js +58 -17
- package/dist/lib/dedicated-machines.d.ts +66 -0
- package/dist/lib/dedicated-machines.js +161 -0
- package/dist/lib/harnesses.d.ts +5 -0
- package/dist/lib/harnesses.js +5 -2
- package/dist/lib/idempotency-scope.d.ts +10 -0
- package/dist/lib/idempotency-scope.js +16 -0
- package/dist/lib/node-adapter.d.ts +1 -1
- package/dist/lib/node-adapter.js +12 -10
- package/dist/lib/task-legacy.d.ts +24 -0
- package/dist/lib/task-legacy.js +48 -0
- package/dist/lib/tasks.d.ts +32 -0
- package/dist/lib/tasks.js +47 -1
- package/dist/lib/triggers.js +7 -0
- package/dist/lib/work-sync.d.ts +105 -0
- package/dist/lib/work-sync.js +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +71 -21
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +71 -21
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2141 -2061
- package/oclif.manifest.json +15661 -14585
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
|
+
const data_provider_2 = require("@skrr-ai/data-provider");
|
|
5
6
|
const base_command_1 = require("../../base-command");
|
|
6
7
|
/**
|
|
7
8
|
* `skrr tasks expectations <task>` — what this task owes, and how far each got.
|
|
@@ -23,16 +24,58 @@ const STATE_LABEL = {
|
|
|
23
24
|
owed: 'not yet',
|
|
24
25
|
unassessed: 'needs assessment',
|
|
25
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The evidence gap, on the rows where a reader can act on it.
|
|
29
|
+
*
|
|
30
|
+
* `unverified` and `underway` BOTH mean "something was produced and pinned to
|
|
31
|
+
* this Expectation, and it is not enough yet" — they differ only in whether the
|
|
32
|
+
* thing has been observed as available. Printing the gap on the first and
|
|
33
|
+
* nothing on the second is what made the careful path unreadable: an operator
|
|
34
|
+
* who pinned a deliverable saw `on its way` and had no way to learn that the
|
|
35
|
+
* bar was `external_receipt` and their own report could never clear it.
|
|
36
|
+
*
|
|
37
|
+
* It stays off the rows where it is noise: a met or waived row has nothing
|
|
38
|
+
* outstanding, and an `owed`/`unassessed` row has no deliverable to compare
|
|
39
|
+
* against, so naming a required authority there describes a hypothetical.
|
|
40
|
+
*/
|
|
41
|
+
const PINNED_STATES = new Set(['unverified', 'underway']);
|
|
42
|
+
function evidenceGap(expectation) {
|
|
43
|
+
/*
|
|
44
|
+
* A `met` row settled BELOW its declared bar says so.
|
|
45
|
+
*
|
|
46
|
+
* `@merged-to-main` asks for `external_receipt` and is satisfiable by a person
|
|
47
|
+
* typing "I checked git log". That is allowed on purpose — refusing it is the
|
|
48
|
+
* deadlock the ladder exists to prevent — but every surface then read as though
|
|
49
|
+
* a receipt had arrived: the row said `done`, the rollup said `met`, and the
|
|
50
|
+
* recorded gap was visible only to someone who dumped the assessment rows and
|
|
51
|
+
* already knew the field existed.
|
|
52
|
+
*/
|
|
53
|
+
if (expectation.evidenceShortfall) {
|
|
54
|
+
const { provided, required } = expectation.evidenceShortfall;
|
|
55
|
+
return ` (vouched at ${provided}, asked for ${required})`;
|
|
56
|
+
}
|
|
57
|
+
if (!PINNED_STATES.has(expectation.state))
|
|
58
|
+
return '';
|
|
59
|
+
if (!expectation.evidenceLevel || !expectation.requiredEvidence)
|
|
60
|
+
return '';
|
|
61
|
+
if (expectation.evidenceLevel === expectation.requiredEvidence)
|
|
62
|
+
return '';
|
|
63
|
+
return ` (has ${expectation.evidenceLevel}, needs ${expectation.requiredEvidence})`;
|
|
64
|
+
}
|
|
26
65
|
function line(expectation) {
|
|
27
66
|
const label = STATE_LABEL[expectation.state] || expectation.state;
|
|
28
67
|
const from = expectation.source === 'space' ? ' [space default]' : '';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
68
|
+
/*
|
|
69
|
+
* `sentence` before `text`.
|
|
70
|
+
*
|
|
71
|
+
* The server resolves `sentence` through `describeExpectation`, which is what
|
|
72
|
+
* appends a preset's destination — so `@wiki-page-published:Engineering/Deploy`
|
|
73
|
+
* and the same preset at `Ops/Oncall` are distinguishable. Printing `text`
|
|
74
|
+
* rendered the preset's fixed copy and dropped the one part that says WHICH
|
|
75
|
+
* thing is owed. The fallback is for a row that predates the resolved field.
|
|
76
|
+
*/
|
|
77
|
+
const sentence = expectation.sentence || expectation.text;
|
|
78
|
+
return ` ${expectation.id.padEnd(18)} ${label.padEnd(18)} ${sentence}${evidenceGap(expectation)}${from}`;
|
|
36
79
|
}
|
|
37
80
|
class TasksExpectations extends base_command_1.BaseCommand {
|
|
38
81
|
static description = 'Show what must be true before the Task can close and the evidence-backed assessment state';
|
|
@@ -79,12 +122,36 @@ class TasksExpectations extends base_command_1.BaseCommand {
|
|
|
79
122
|
this.log(line(expectation));
|
|
80
123
|
}
|
|
81
124
|
const gating = acceptance.expectations.filter((e) => e.gating);
|
|
82
|
-
|
|
125
|
+
// `met: true` is the GATE's answer — a waiver unblocks closure exactly as a
|
|
126
|
+
// pass does — but it is the wrong REPORT. Counting them together printed
|
|
127
|
+
// "2 of 2 met" for a Task where one expectation was dropped, which is the
|
|
128
|
+
// one thing a reader checking this line needs to know.
|
|
129
|
+
const waived = gating.filter((e) => e.state === 'waived').length;
|
|
130
|
+
const met = gating.filter((e) => e.met).length - waived;
|
|
83
131
|
this.log('');
|
|
84
|
-
this.log(`${met} of ${gating.length} met · overall: ${acceptance.state}`);
|
|
132
|
+
this.log(`${met} of ${gating.length} met${waived > 0 ? ` · ${waived} waived` : ''} · overall: ${acceptance.state}`);
|
|
85
133
|
// Space defaults are copied into the Task at creation. `source=space`
|
|
86
134
|
// remains useful provenance, but these are Task-owned Expectations now and
|
|
87
135
|
// are assessed exactly like explicitly declared ones.
|
|
136
|
+
/*
|
|
137
|
+
* A typed check nothing evaluates behaves exactly like `manual`.
|
|
138
|
+
*
|
|
139
|
+
* `EXPECTATION_CHECK_METHODS` declares six methods and the judge evaluates
|
|
140
|
+
* one: a `deliverable` check is compared against the ledger, and
|
|
141
|
+
* `task_state`, `goal_state`, `metric` and `llm_review` return `unassessed`
|
|
142
|
+
* forever. Verified against production — a `metric` check naming measureId,
|
|
143
|
+
* operator and target stores fine and is never looked at. The row read
|
|
144
|
+
* identically to a hand-written sentence, so somebody who chose
|
|
145
|
+
* `metric: cycle_time lt 2` had no way to learn the platform was not
|
|
146
|
+
* measuring it (OSK-8488).
|
|
147
|
+
*/
|
|
148
|
+
const unevaluated = acceptance.expectations.filter((e) => (0, data_provider_2.declaresUnevaluatedCheck)(e));
|
|
149
|
+
if (unevaluated.length > 0) {
|
|
150
|
+
const methods = [...new Set(unevaluated.map((e) => e.check?.method).filter(Boolean))].join(', ');
|
|
151
|
+
this.log(`${unevaluated.length} declare${unevaluated.length === 1 ? 's' : ''} a typed check nothing ` +
|
|
152
|
+
`evaluates yet (${methods}) — a person settles ${unevaluated.length === 1 ? 'it' : 'them'} ` +
|
|
153
|
+
'exactly as if the check said `manual`.');
|
|
154
|
+
}
|
|
88
155
|
const settleable = acceptance.expectations.filter((e) => e.state === 'unassessed').length;
|
|
89
156
|
if (settleable > 0) {
|
|
90
157
|
this.log(`${settleable} awaiting assessment — use \`${this.config.bin} tasks expectations assess\`.`);
|
|
@@ -98,7 +165,40 @@ class TasksExpectations extends base_command_1.BaseCommand {
|
|
|
98
165
|
`\`${this.config.bin} tasks deliverable confirm\`, or lower the bar with ` +
|
|
99
166
|
`\`${this.config.bin} tasks trust\`.`);
|
|
100
167
|
}
|
|
101
|
-
|
|
168
|
+
// `underway` had no trailer at all, which is why pinning a deliverable read
|
|
169
|
+
// as no progress: the row said `on its way` and nothing named the lever.
|
|
170
|
+
// A Deliverable is recorded `local` by default, so this is the state the
|
|
171
|
+
// ORDINARY `tasks deliverable add` lands in — the common case, not an edge.
|
|
172
|
+
const underway = acceptance.expectations.filter((e) => e.state === 'underway').length;
|
|
173
|
+
if (underway > 0) {
|
|
174
|
+
this.log(`${underway} recorded but not yet observed as available — say it arrived with ` +
|
|
175
|
+
`\`${this.config.bin} tasks deliverable confirm\`, or re-record it with ` +
|
|
176
|
+
`\`--status available\`.`);
|
|
177
|
+
}
|
|
178
|
+
// Counted separately from the unmet ones: these no longer block anything, and
|
|
179
|
+
// the reader's question about them is not "what do I do" but "how good is this
|
|
180
|
+
// record". Saying it once beats repeating the rungs on every row.
|
|
181
|
+
const shortfalls = acceptance.expectations.filter((e) => e.evidenceShortfall).length;
|
|
182
|
+
if (shortfalls > 0) {
|
|
183
|
+
this.log(`${shortfalls} settled below the evidence ${shortfalls === 1 ? 'bar it' : 'bars they'} ` +
|
|
184
|
+
`declared — allowed, and recorded as such on the assessment.`);
|
|
185
|
+
}
|
|
186
|
+
// Said once, for the whole task: `external_receipt` is the one bar no CLI
|
|
187
|
+
// command can clear. The only receipt producer in the platform is PR
|
|
188
|
+
// integration, so an operator staring at a row that needs one would
|
|
189
|
+
// otherwise keep looking for the command that supplies it.
|
|
190
|
+
const receiptBarred = acceptance.expectations.filter((e) => !e.met && e.requiredEvidence === 'external_receipt').length;
|
|
191
|
+
if (receiptBarred > 0) {
|
|
192
|
+
this.log(`${receiptBarred} ask${receiptBarred === 1 ? 's' : ''} for an external provider receipt, ` +
|
|
193
|
+
`which only an integration can supply — settle by hand with ` +
|
|
194
|
+
`\`${this.config.bin} tasks expectations assess\` (the gap is recorded), or lower the ` +
|
|
195
|
+
`bar with \`${this.config.bin} tasks trust\`.`);
|
|
196
|
+
}
|
|
197
|
+
// Settled, not "met" — a waiver settles an Expectation as surely as a pass,
|
|
198
|
+
// and splitting the two in the count above must not make this trailer fire
|
|
199
|
+
// on a Task where nothing is actually outstanding.
|
|
200
|
+
const settled = gating.filter((e) => e.met).length;
|
|
201
|
+
if (gating.length > settled) {
|
|
102
202
|
this.log('');
|
|
103
203
|
this.log('The current Result cannot be accepted while these Expectations remain unmet.');
|
|
104
204
|
}
|
|
@@ -7,6 +7,19 @@ const task_resolver_1 = require("../../lib/task-resolver");
|
|
|
7
7
|
const web_url_1 = require("../../lib/web-url");
|
|
8
8
|
const task_transcript_1 = require("../../lib/task-transcript");
|
|
9
9
|
/** Compatibility command name. The authoritative object is Result, not output. */
|
|
10
|
+
/**
|
|
11
|
+
* "awaiting review" is only true while a review can still be recorded.
|
|
12
|
+
*
|
|
13
|
+
* A closed Task refuses one outright (`TASK_REOPEN_REQUIRED`), so printing
|
|
14
|
+
* "awaiting" described a state that could never be left — and the reader has no
|
|
15
|
+
* way to tell that from a review that is genuinely pending.
|
|
16
|
+
*/
|
|
17
|
+
function reviewLine(review, task) {
|
|
18
|
+
if (review?.decision)
|
|
19
|
+
return review.decision;
|
|
20
|
+
const terminal = ['completed', 'cancelled'].includes(String(task?.statusType ?? ''));
|
|
21
|
+
return terminal ? 'none recorded — the Task is closed' : 'awaiting review';
|
|
22
|
+
}
|
|
10
23
|
class TasksOutput extends base_command_1.BaseCommand {
|
|
11
24
|
static hidden = true;
|
|
12
25
|
static description = 'Show the current Result, Review, Deliverables, and final Run reply';
|
|
@@ -93,10 +106,17 @@ class TasksOutput extends base_command_1.BaseCommand {
|
|
|
93
106
|
if (result.conclusion) {
|
|
94
107
|
this.log(` conclusion (${result.conclusion.type}): ${JSON.stringify(result.conclusion.value)}`);
|
|
95
108
|
}
|
|
96
|
-
this.log(`Review: ${review
|
|
109
|
+
this.log(`Review: ${reviewLine(review, task)}`);
|
|
97
110
|
if (review?.rationale)
|
|
98
111
|
this.log(` ${review.rationale}`);
|
|
99
112
|
}
|
|
113
|
+
else if (task.specVersion === undefined) {
|
|
114
|
+
// A Task predating the canonical model has no TaskResult row and never
|
|
115
|
+
// will. "none submitted" reads as "the work was never reported", which
|
|
116
|
+
// for a completed legacy Task is the opposite of the truth.
|
|
117
|
+
this.log('Result: not recorded under the canonical model (this Task predates it)');
|
|
118
|
+
this.log(' its closeout is in `skrr tasks comments list` / `tasks transcript`');
|
|
119
|
+
}
|
|
100
120
|
else {
|
|
101
121
|
this.log('Result: none submitted');
|
|
102
122
|
}
|
|
@@ -14,7 +14,9 @@ const web_url_1 = require("../../lib/web-url");
|
|
|
14
14
|
*/
|
|
15
15
|
class TasksReport extends base_command_1.BaseCommand {
|
|
16
16
|
static hidden = true;
|
|
17
|
-
static description = '
|
|
17
|
+
static description = 'DEPRECATED compatibility spelling for `tasks result submit`. Every call submits a ' +
|
|
18
|
+
'versioned Result; `--kind` is ignored. Use `tasks result submit` (submit only) or ' +
|
|
19
|
+
'`tasks complete` (submit and close).';
|
|
18
20
|
static examples = [
|
|
19
21
|
'<%= config.bin %> tasks report <task-id> --outcome "Adopt the event ledger" --finding "The task page omits work-event details" --evidence "Task detail reads only legacy activity" --next "Add a Result section"',
|
|
20
22
|
'<%= config.bin %> tasks report <task-id> --kind handoff --outcome "Ready for review" --finding "All expectations are met" --deliverable "file:src/task.ts" --json',
|
|
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
5
|
const base_command_1 = require("../../../base-command");
|
|
6
6
|
const task_resolver_1 = require("../../../lib/task-resolver");
|
|
7
|
+
const task_legacy_1 = require("../../../lib/task-legacy");
|
|
7
8
|
class TasksResultShow extends base_command_1.BaseCommand {
|
|
8
9
|
static description = 'Show the current Task Result and its exact Review';
|
|
9
10
|
static args = {
|
|
@@ -29,12 +30,29 @@ class TasksResultShow extends base_command_1.BaseCommand {
|
|
|
29
30
|
return this.log(JSON.stringify(result, null, 2));
|
|
30
31
|
const current = result.results.find((row) => row.id === result.currentResultId);
|
|
31
32
|
if (!current)
|
|
32
|
-
return this.log(
|
|
33
|
+
return this.log(await (0, task_legacy_1.emptyCanonicalView)(task.id, 'Result'));
|
|
33
34
|
const review = result.reviews.find((row) => row.resultId === current.id);
|
|
34
35
|
this.log(`Result v${current.version}\n${current.summary}`);
|
|
35
36
|
if (current.conclusion)
|
|
36
37
|
this.log(`Conclusion (${current.conclusion.type}): ${JSON.stringify(current.conclusion.value)}`);
|
|
37
|
-
|
|
38
|
+
// Only "awaiting" while a review can still be recorded. A closed Task
|
|
39
|
+
// refuses one outright (`TASK_REOPEN_REQUIRED`), so the old wording named a
|
|
40
|
+
// state nothing could ever leave — and a reader could not tell it from a
|
|
41
|
+
// review that is genuinely pending. The extra read happens only on the
|
|
42
|
+
// no-review path, where the distinction is the whole point.
|
|
43
|
+
let reviewLine = 'awaiting review';
|
|
44
|
+
if (review) {
|
|
45
|
+
reviewLine = review.decision;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const full = (await data_provider_1.dataService
|
|
49
|
+
.getTask(task.id, { enrich: false })
|
|
50
|
+
.catch(() => undefined));
|
|
51
|
+
if (['completed', 'cancelled'].includes(String(full?.statusType ?? ''))) {
|
|
52
|
+
reviewLine = 'none recorded — the Task is closed';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.log(`Review: ${reviewLine}`);
|
|
38
56
|
if (flags.history && result.results.length > 1) {
|
|
39
57
|
this.log(`Superseded versions: ${result.results
|
|
40
58
|
.slice(1)
|
|
@@ -15,7 +15,17 @@ class TasksResumeSave extends base_command_1.BaseCommand {
|
|
|
15
15
|
static flags = {
|
|
16
16
|
task: core_1.Flags.string({ description: 'Task id; overrides runtime context' }),
|
|
17
17
|
'run-id': core_1.Flags.string({ required: true, description: 'Canonical Task Run' }),
|
|
18
|
-
snapshot: core_1.Flags.string({
|
|
18
|
+
snapshot: core_1.Flags.string({
|
|
19
|
+
required: true,
|
|
20
|
+
description: 'Resume Point JSON snapshot. ' +
|
|
21
|
+
'Every field is optional. `completed`, `nextActions` and `currentFocus` accept either a ' +
|
|
22
|
+
'plain sentence or {summary,sourceRefs} — the object form is canonical and a bare string ' +
|
|
23
|
+
'is lifted into it, so the same sentence you put in an Update’s nextActions works here ' +
|
|
24
|
+
'unchanged. Shape: {completed:[], currentFocus, nextActions:[], ' +
|
|
25
|
+
'openQuestions:[{question,sourceRef?}], relevantUpdateIds:[], evidenceRefs:[], ' +
|
|
26
|
+
'deliverableRefs:[], childHeads:[{runId,resumePointId,status}], workspaceStateRef}. ' +
|
|
27
|
+
'Never put a credential, token or lease in a snapshot — the server refuses one.',
|
|
28
|
+
}),
|
|
19
29
|
'spec-version': core_1.Flags.integer({ description: 'Task specification version' }),
|
|
20
30
|
'idempotency-key': core_1.Flags.string({ description: 'Stable retry key' }),
|
|
21
31
|
json: core_1.Flags.boolean({ description: 'Output JSON' }),
|
|
@@ -31,10 +31,13 @@ class TasksSelfSchedule extends base_command_1.BaseCommand {
|
|
|
31
31
|
at: core_1.Flags.string({ description: 'Absolute return time (ISO-8601 UTC)' }),
|
|
32
32
|
in: core_1.Flags.string({ description: 'Relative return time such as 90m, 2h, 3d' }),
|
|
33
33
|
note: core_1.Flags.string({
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
34
|
+
// This DOES reach the run now. It is read by `buildTaskExecutionPrompt`,
|
|
35
|
+
// the one place every dispatch path assembles a task prompt, and rendered
|
|
36
|
+
// as `### Why You Parked This Task` alongside the instant it was parked
|
|
37
|
+
// for. For a while it was recorded and never delivered, and the help said
|
|
38
|
+
// so; both halves are fixed rather than only the sentence.
|
|
39
|
+
description: 'Why you are coming back. Delivered to the agent on the return as its own ' +
|
|
40
|
+
'prompt section, and readable any time with --show',
|
|
38
41
|
}),
|
|
39
42
|
'max-per-day': core_1.Flags.integer({
|
|
40
43
|
description: 'Cap on self-initiated returns per day (1-24, default 4)',
|
|
@@ -262,6 +262,25 @@ class TasksShow extends base_command_1.BaseCommand {
|
|
|
262
262
|
this.log(`Created: ${formatTime(t.createdAt)}`);
|
|
263
263
|
if (t.updatedAt)
|
|
264
264
|
this.log(`Updated: ${formatTime(t.updatedAt)}`);
|
|
265
|
+
/**
|
|
266
|
+
* Say which Task model this row belongs to, on the one surface everybody
|
|
267
|
+
* reads first.
|
|
268
|
+
*
|
|
269
|
+
* Canonical Task Model v1 deliberately migrated nothing, so a Task created
|
|
270
|
+
* before it is a second class of object with visibly different behaviour —
|
|
271
|
+
* no Updates, no Result, no Activity, no timeline. The only marker was
|
|
272
|
+
* `specVersion` being ABSENT under `--json`, and absence-as-a-marker is not
|
|
273
|
+
* something a reader can notice: you have to already know the field exists
|
|
274
|
+
* to observe that it does not. So `tasks show` said nothing, and the user
|
|
275
|
+
* found out by watching four other commands report emptiness about a Task
|
|
276
|
+
* with years of history.
|
|
277
|
+
*
|
|
278
|
+
* Printed only for the legacy class. A canonical Task needs no line saying
|
|
279
|
+
* it is ordinary.
|
|
280
|
+
*/
|
|
281
|
+
if (t.specVersion === undefined || t.specVersion === null) {
|
|
282
|
+
this.log('Model: predates the canonical Task model — no Result, Updates, Activity or timeline');
|
|
283
|
+
}
|
|
265
284
|
if (t.description) {
|
|
266
285
|
this.log('');
|
|
267
286
|
this.log('Description:');
|
|
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
|
|
|
5
5
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
6
|
const base_command_1 = require("../../base-command");
|
|
7
7
|
const format_1 = require("../../lib/format");
|
|
8
|
+
const task_legacy_1 = require("../../lib/task-legacy");
|
|
8
9
|
/**
|
|
9
10
|
* `<action> (<k=v ...>)`, or just the preview when there is nothing to add.
|
|
10
11
|
*
|
|
@@ -49,7 +50,14 @@ class TasksTimeline extends base_command_1.BaseCommand {
|
|
|
49
50
|
this.log(JSON.stringify(result, null, 2));
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
const rows = result.data ?? [];
|
|
54
|
+
if (rows.length === 0) {
|
|
55
|
+
// `renderTable([])` prints NOTHING — no header, no empty state, exit 0 —
|
|
56
|
+
// which is indistinguishable from a broken command. Say what happened.
|
|
57
|
+
this.log(await (0, task_legacy_1.emptyCanonicalView)(args.id, 'timeline entries'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
(0, format_1.renderTable)(rows.map((item) => ({
|
|
53
61
|
time: String(item.occurredAt ?? '-')
|
|
54
62
|
.replace('T', ' ')
|
|
55
63
|
.slice(0, 19),
|
|
@@ -21,6 +21,9 @@ class TasksUpdatesAdd extends base_command_1.BaseCommand {
|
|
|
21
21
|
static examples = [
|
|
22
22
|
'<%= config.bin %> tasks updates add <task> --type progress --summary "Migration review complete" --data \'{"completedSinceLast":["Checked migrations 001-014"],"nextActions":["Review authorization"]}\'',
|
|
23
23
|
'<%= config.bin %> tasks updates add <task> --type decision --summary "Use a resource object" --data \'{"decision":"Use WorkResource","rationale":"Identity must survive URL changes"}\'',
|
|
24
|
+
'<%= config.bin %> tasks updates add <task> --type finding --summary "Cross-tenant leak" --data \'{"finding":"Space ACL is not applied on the list path","impact":"Any member can read another space","severity":"high","evidenceRefs":[]}\'',
|
|
25
|
+
'<%= config.bin %> tasks updates add <task> --type blocker --summary "Needs a prod credential" --data \'{"cause":"No Atlas API key in this runtime","requiredAction":"Provision a read-only key","nextOwner":{"kind":"human","userId":"<user-id>"}}\'',
|
|
26
|
+
'<%= config.bin %> tasks updates add <task> --type verification --summary "Repro is gone" --data \'{"claim":"The 409 no longer reproduces","result":"passed","evidenceRefs":[]}\'',
|
|
24
27
|
];
|
|
25
28
|
static args = {
|
|
26
29
|
task: core_1.Args.string({ required: false, ignoreStdin: true, description: 'Task id or ref' }),
|
|
@@ -33,9 +36,27 @@ class TasksUpdatesAdd extends base_command_1.BaseCommand {
|
|
|
33
36
|
description: 'What changed in the shared understanding of the Task',
|
|
34
37
|
}),
|
|
35
38
|
summary: core_1.Flags.string({ required: true, description: 'One sentence saying what became true' }),
|
|
36
|
-
data: core_1.Flags.string({
|
|
39
|
+
data: core_1.Flags.string({
|
|
40
|
+
required: true,
|
|
41
|
+
description: 'Kind-specific JSON payload. The server enforces one shape per --type, and the array ' +
|
|
42
|
+
'fields are required even when empty. ' +
|
|
43
|
+
'progress {completedSinceLast:[],nextActions:[],currentFocus?} · ' +
|
|
44
|
+
'finding {finding,evidenceRefs:[],impact?,severity?} · ' +
|
|
45
|
+
'decision {decision,rationale,alternativesConsidered?} · ' +
|
|
46
|
+
'blocker {cause,requiredAction,nextOwner} · ' +
|
|
47
|
+
'verification {claim,result:passed|failed|inconclusive,evidenceRefs:[]}. ' +
|
|
48
|
+
'nextOwner is a principal object like {"kind":"human","userId":"..."}, not a name. ' +
|
|
49
|
+
"Note --reference sets the Update's own references, NOT data.evidenceRefs.",
|
|
50
|
+
}),
|
|
37
51
|
resume: core_1.Flags.string({
|
|
38
|
-
description: 'JSON recovery snapshot; requires --run-id and is committed with the Update'
|
|
52
|
+
description: 'JSON recovery snapshot; requires --run-id and is committed with the Update. ' +
|
|
53
|
+
'Every field is optional. `completed`, `nextActions` and `currentFocus` accept either a ' +
|
|
54
|
+
'plain sentence or {summary,sourceRefs} — the object form is canonical and a bare string ' +
|
|
55
|
+
'is lifted into it, so the same sentence you put in an Update’s nextActions works here ' +
|
|
56
|
+
'unchanged. Shape: {completed:[], currentFocus, nextActions:[], ' +
|
|
57
|
+
'openQuestions:[{question,sourceRef?}], relevantUpdateIds:[], evidenceRefs:[], ' +
|
|
58
|
+
'deliverableRefs:[], childHeads:[{runId,resumePointId,status}], workspaceStateRef}. ' +
|
|
59
|
+
'Never put a credential, token or lease in a snapshot — the server refuses one.',
|
|
39
60
|
}),
|
|
40
61
|
'run-id': core_1.Flags.string({ description: 'Canonical Task Run that performed this work' }),
|
|
41
62
|
'spec-version': core_1.Flags.integer({
|
package/dist/lib/api-fetch.js
CHANGED
|
@@ -37,6 +37,8 @@ const config_1 = require("./config");
|
|
|
37
37
|
// (b) tests that mock './auth-storage' get a single mock target.
|
|
38
38
|
const auth_storage_1 = require("./auth-storage");
|
|
39
39
|
const credential_resolver_1 = require("./credential-resolver");
|
|
40
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
41
|
+
const idempotency_scope_1 = require("./idempotency-scope");
|
|
40
42
|
const node_adapter_1 = require("./node-adapter");
|
|
41
43
|
const refresh_1 = require("./refresh");
|
|
42
44
|
const delegated_cli_1 = require("./delegated-cli");
|
|
@@ -95,6 +97,12 @@ async function apiFetch(pathOrUrl, opts = {}) {
|
|
|
95
97
|
if (opts.body !== undefined && method !== 'GET') {
|
|
96
98
|
body = JSON.stringify(opts.body);
|
|
97
99
|
}
|
|
100
|
+
// Same rule as every `dataService` write (`@skrr-ai/data-provider`
|
|
101
|
+
// idempotency), chosen once so the 401 retries below resend the same key.
|
|
102
|
+
(0, idempotency_scope_1.bindAutonomousIdempotencyScope)();
|
|
103
|
+
const callerHeaders = method === 'GET'
|
|
104
|
+
? (opts.headers ?? {})
|
|
105
|
+
: (0, data_provider_1.withIdempotencyKey)(opts.headers, { method, url: pathOrUrl, body: opts.body });
|
|
98
106
|
// A trusted Agent process cannot smuggle an explicit human credential into
|
|
99
107
|
// this escape hatch. The daemon-issued session token is the sole principal.
|
|
100
108
|
//
|
|
@@ -146,7 +154,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
|
|
|
146
154
|
// Caller headers are applied BEFORE the auth header below is read, and
|
|
147
155
|
// deliberately cannot clobber Authorization / Content-Type: an idempotency
|
|
148
156
|
// key is additive metadata, not a way to re-auth the request.
|
|
149
|
-
for (const [k, v] of Object.entries(
|
|
157
|
+
for (const [k, v] of Object.entries(callerHeaders)) {
|
|
150
158
|
const lower = k.toLowerCase();
|
|
151
159
|
if (lower === 'authorization' || lower === 'content-type')
|
|
152
160
|
continue;
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* reference into exactly one member of the task's space, or an actionable
|
|
10
10
|
* failure that names the candidates.
|
|
11
11
|
*
|
|
12
|
-
* Scoping to the space is not just ergonomics. The server
|
|
13
|
-
*
|
|
14
|
-
* → `ASSIGNEE_NOT_IN_SPACE`), so
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* Scoping to the space is not just ergonomics. The server accepts space members
|
|
13
|
+
* and, beyond them, only the caller's own agent when it is shared with no one
|
|
14
|
+
* else (`AgentTaskConsent` → otherwise `ASSIGNEE_NOT_IN_SPACE`), so a name that
|
|
15
|
+
* resolves against the membership list is a name that will be accepted, and we
|
|
16
|
+
* cannot accidentally leak a directory-wide user search into a space the caller
|
|
17
|
+
* can't see. An owner-delegated agent is not a member and so is not resolvable
|
|
18
|
+
* by name here; it is passed by raw id.
|
|
18
19
|
*/
|
|
19
20
|
export type MemberKind = 'user' | 'agent';
|
|
20
21
|
export type SpaceMember = {
|
|
@@ -139,6 +139,8 @@ export interface DaemonBrokerOptions {
|
|
|
139
139
|
profile?: string;
|
|
140
140
|
/** Override the bootstrap file path entirely (tests / unusual layouts). */
|
|
141
141
|
bootstrapPathOverride?: string;
|
|
142
|
+
/** Test seam: the ordered candidate list, when one path is not enough. */
|
|
143
|
+
candidatePathsOverride?: readonly string[];
|
|
142
144
|
/** Override total request timeout. Tests use a short value. */
|
|
143
145
|
timeoutMs?: number;
|
|
144
146
|
}
|
|
@@ -159,6 +161,24 @@ export declare function resolveProfileBootstrapPaths(primary: string, entryNames
|
|
|
159
161
|
* deterministic when more than one matching daemon is running.
|
|
160
162
|
*/
|
|
161
163
|
export declare function resolveBootstrapCandidatePaths(profile?: string): string[];
|
|
164
|
+
/**
|
|
165
|
+
* A Dedicated Runtime guest's workload hand-off descriptor.
|
|
166
|
+
*
|
|
167
|
+
* On that product the daemon runs as `skrr-runtime` and everything the user runs
|
|
168
|
+
* — terminals, Bash tool calls, engines — as `skrr-workload`, so the island
|
|
169
|
+
* bootstrap above sits in a home this process cannot read. The daemon publishes
|
|
170
|
+
* this second descriptor to the shared workload group instead
|
|
171
|
+
* (`daemon/src/dedicated-cli-handoff.ts`). Its secret is accepted by the
|
|
172
|
+
* cli-handoff route and nothing else, so it is a hand-off candidate ONLY: it is
|
|
173
|
+
* deliberately absent from `resolveBootstrapCandidatePaths`, which message-intent
|
|
174
|
+
* submission also reads and which needs the island secret.
|
|
175
|
+
*
|
|
176
|
+
* Fixed and root-controlled (the daemon unit's RuntimeDirectory), so nothing the
|
|
177
|
+
* user configures can point the broker somewhere else.
|
|
178
|
+
*/
|
|
179
|
+
export declare const DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = "/run/skrr-dedicated-runtime/cli-handoff/descriptor.json";
|
|
180
|
+
/** Where the cli-handoff broker looks, in order. */
|
|
181
|
+
export declare function resolveCliHandoffCandidatePaths(profile?: string): string[];
|
|
162
182
|
/**
|
|
163
183
|
* Resolve the first live daemon bootstrap for a profile. Message-intent
|
|
164
184
|
* submission reuses the same authenticated loopback discovery as the login
|
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -71,10 +71,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
71
71
|
};
|
|
72
72
|
})();
|
|
73
73
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74
|
+
exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = void 0;
|
|
74
75
|
exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
75
76
|
exports.resolveBootstrapPath = resolveBootstrapPath;
|
|
76
77
|
exports.resolveProfileBootstrapPaths = resolveProfileBootstrapPaths;
|
|
77
78
|
exports.resolveBootstrapCandidatePaths = resolveBootstrapCandidatePaths;
|
|
79
|
+
exports.resolveCliHandoffCandidatePaths = resolveCliHandoffCandidatePaths;
|
|
78
80
|
exports.findLiveDaemonBootstrap = findLiveDaemonBootstrap;
|
|
79
81
|
exports.readBootstrap = readBootstrap;
|
|
80
82
|
exports.attemptDaemonBrokerLogin = attemptDaemonBrokerLogin;
|
|
@@ -164,6 +166,33 @@ function resolveBootstrapCandidatePaths(profile = 'default') {
|
|
|
164
166
|
return process.platform === 'darwin' ? [primary] : [primary, legacyBootstrapPath()];
|
|
165
167
|
}
|
|
166
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* A Dedicated Runtime guest's workload hand-off descriptor.
|
|
171
|
+
*
|
|
172
|
+
* On that product the daemon runs as `skrr-runtime` and everything the user runs
|
|
173
|
+
* — terminals, Bash tool calls, engines — as `skrr-workload`, so the island
|
|
174
|
+
* bootstrap above sits in a home this process cannot read. The daemon publishes
|
|
175
|
+
* this second descriptor to the shared workload group instead
|
|
176
|
+
* (`daemon/src/dedicated-cli-handoff.ts`). Its secret is accepted by the
|
|
177
|
+
* cli-handoff route and nothing else, so it is a hand-off candidate ONLY: it is
|
|
178
|
+
* deliberately absent from `resolveBootstrapCandidatePaths`, which message-intent
|
|
179
|
+
* submission also reads and which needs the island secret.
|
|
180
|
+
*
|
|
181
|
+
* Fixed and root-controlled (the daemon unit's RuntimeDirectory), so nothing the
|
|
182
|
+
* user configures can point the broker somewhere else.
|
|
183
|
+
*/
|
|
184
|
+
exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = '/run/skrr-dedicated-runtime/cli-handoff/descriptor.json';
|
|
185
|
+
/** Where the cli-handoff broker looks, in order. */
|
|
186
|
+
function resolveCliHandoffCandidatePaths(profile = 'default') {
|
|
187
|
+
const candidates = resolveBootstrapCandidatePaths(profile);
|
|
188
|
+
// Any profile: a guest runs one daemon, and the auto-broker resolves the
|
|
189
|
+
// default profile regardless of `--profile`, so a narrower condition here would
|
|
190
|
+
// only describe behaviour production never reaches.
|
|
191
|
+
if (process.platform === 'linux') {
|
|
192
|
+
candidates.push(exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR);
|
|
193
|
+
}
|
|
194
|
+
return candidates;
|
|
195
|
+
}
|
|
167
196
|
/**
|
|
168
197
|
* Pre-Phase-K bootstrap path (non-darwin). Read-only fallback consulted
|
|
169
198
|
* when the canonical path is absent — covers a freshly-upgraded daemon
|
|
@@ -247,13 +276,15 @@ function readBootstrap(filePath) {
|
|
|
247
276
|
*/
|
|
248
277
|
async function attemptDaemonBrokerLogin(opts) {
|
|
249
278
|
const profile = opts.profile ?? 'default';
|
|
250
|
-
const candidates = opts.
|
|
251
|
-
? [opts.
|
|
252
|
-
:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
const candidates = opts.candidatePathsOverride
|
|
280
|
+
? [...opts.candidatePathsOverride]
|
|
281
|
+
: opts.bootstrapPathOverride
|
|
282
|
+
? [opts.bootstrapPathOverride]
|
|
283
|
+
: resolveCliHandoffCandidatePaths(profile);
|
|
284
|
+
// Bound candidates (their serverUrl matches this CLI) in order, then unbound
|
|
285
|
+
// ones. Every usable candidate is kept, not just the first: see below.
|
|
286
|
+
const usable = [];
|
|
287
|
+
const unbound = [];
|
|
257
288
|
let mismatchedBootstrap = null;
|
|
258
289
|
for (const candidate of candidates) {
|
|
259
290
|
const candidateBootstrap = readBootstrap(candidate);
|
|
@@ -269,19 +300,13 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
269
300
|
}
|
|
270
301
|
if (typeof candidateBootstrap.serverUrl !== 'string' ||
|
|
271
302
|
candidateBootstrap.serverUrl.length === 0) {
|
|
272
|
-
|
|
273
|
-
unboundPath ??= candidate;
|
|
303
|
+
unbound.push(candidateBootstrap);
|
|
274
304
|
continue;
|
|
275
305
|
}
|
|
276
|
-
|
|
277
|
-
usedPath = candidate;
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
if (!bootstrap && unboundBootstrap && unboundPath) {
|
|
281
|
-
bootstrap = unboundBootstrap;
|
|
282
|
-
usedPath = unboundPath;
|
|
306
|
+
usable.push(candidateBootstrap);
|
|
283
307
|
}
|
|
284
|
-
|
|
308
|
+
usable.push(...unbound);
|
|
309
|
+
if (usable.length === 0) {
|
|
285
310
|
if (mismatchedBootstrap?.serverUrl) {
|
|
286
311
|
return {
|
|
287
312
|
ok: false,
|
|
@@ -296,6 +321,22 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
296
321
|
detail: `No daemon bootstrap file found (looked in ${candidates.join(', ')})`,
|
|
297
322
|
};
|
|
298
323
|
}
|
|
324
|
+
// A candidate nothing is listening behind moves on to the next one. Liveness is
|
|
325
|
+
// only a pid check, and EPERM counts as alive — on a Dedicated Runtime guest
|
|
326
|
+
// nearly every pid belongs to another uid, so a stale island file in the
|
|
327
|
+
// workload's own home (restored dotfiles, say) passes it and used to shadow the
|
|
328
|
+
// daemon's live hand-off descriptor forever, with `skrr` reporting "Not signed
|
|
329
|
+
// in". Only a refused connection falls through: any answer, or a timeout, is a
|
|
330
|
+
// daemon that is really there, and its outcome stands.
|
|
331
|
+
let outcome = null;
|
|
332
|
+
for (const bootstrap of usable) {
|
|
333
|
+
outcome = await brokerThrough(bootstrap, opts);
|
|
334
|
+
if (outcome.ok || outcome.reason !== 'network')
|
|
335
|
+
return outcome;
|
|
336
|
+
}
|
|
337
|
+
return outcome;
|
|
338
|
+
}
|
|
339
|
+
async function brokerThrough(bootstrap, opts) {
|
|
299
340
|
const url = `http://${bootstrap.host}:${bootstrap.port}/v1/auth/cli-handoff`;
|
|
300
341
|
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
301
342
|
const controller = new AbortController();
|