software-defence-factory 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/software-defence-factory.mjs +38 -19
- package/docs/interfaces.md +53 -4
- package/docs/quickstart.md +2 -3
- package/docs/setup.md +34 -1
- package/docs/workflows.md +41 -10
- package/factory/definition.mjs +2 -1
- package/factory/execution-profile.mjs +2 -2
- package/factory/issue-intake.mjs +13 -10
- package/factory/issue-lifecycle.mjs +59 -0
- package/factory/lib.mjs +2 -0
- package/factory/providers/github.mjs +2 -2
- package/factory/queue.mjs +40 -6
- package/factory/server.mjs +28 -9
- package/factory/ui/assets/index-BQZGHA6-.css +1 -0
- package/factory/ui/assets/index-BUCK1R7u.js +13 -0
- package/factory/ui/index.html +2 -2
- package/package.json +1 -1
- package/factory/ui/assets/index-C9gpwCUM.js +0 -13
- package/factory/ui/assets/index-CasRG9Ge.css +0 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ The runtime supplies policy and six focused skills to its isolated jobs. `init`
|
|
|
37
37
|
|
|
38
38
|
Each result belongs to a specific candidate commit and policy. A failed check blocks delivery. Changing the candidate or check policy invalidates earlier evidence. Approval records a handoff; publishing, merging and deployment follow the application's separate authority.
|
|
39
39
|
|
|
40
|
-
The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**.
|
|
40
|
+
The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**. Inbox lists repository issues with readiness and linked execution attempts. New issue offers repository templates or a blank creation form. Create an issue on the supported repository provider, then choose Start work separately; local brief execution remains available. CLI `issue` exposes the same intake. Definition lives with settings above the theme control. The CLI reads the same definition and controller state. Agent roles use a selected harness such as Codex or Pi; a worker executes their isolated jobs on a host. See [concepts](docs/concepts.md) and [supported interfaces](docs/interfaces.md). Optional automations belong to the selected harness, which calls Factory CLI/API. Factory runs no cron scheduler. See [provider boundaries](docs/integrations.md).
|
|
41
41
|
|
|
42
42
|
The optional **defence** workflow accepts scoped incident evidence and produces a private, read-only draft. It does not monitor production or claim verified recovery. See [defence integration](docs/defence-integration.md).
|
|
43
43
|
|
|
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process';
|
|
|
6
6
|
import { createServer } from 'node:net';
|
|
7
7
|
import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers, PUBLICATION_API_TIMEOUT_MS } from '../factory/lib.mjs';
|
|
8
8
|
import { assertInstalledJobImage, installCustomJobImage, installStandardJobImage, inspectImageInstallation } from '../factory/image-install.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { readIssue } from '../factory/issue-intake.mjs';
|
|
10
10
|
import { recommendWork } from '../factory/intake.mjs';
|
|
11
11
|
import { factoryDefinition, foundationSkill } from '../factory/definition.mjs';
|
|
12
12
|
import { harnessOf } from '../factory/lib.mjs';
|
|
@@ -55,6 +55,16 @@ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD', deli
|
|
|
55
55
|
console.log(`Configured ${state}\nApp files were not changed. Only committed code is cloned into jobs.`);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
async function listInbox(defaultSource) {
|
|
59
|
+
const source=flags.source || defaultSource;
|
|
60
|
+
if(!['factory','inbox','remote','github'].includes(source))throw new Error('Choose --source factory, inbox or remote');
|
|
61
|
+
if(source==='factory') {
|
|
62
|
+
if(flags.page !== undefined || flags['issue-state'] !== undefined)throw new Error('Repository paging/state filters require --source inbox or remote.');
|
|
63
|
+
return (await api(state,'/api/v1/status')).jobs;
|
|
64
|
+
}
|
|
65
|
+
return api(state,`/api/v1/issues?page=${encodeURIComponent(flags.page || 1)}&state=${encodeURIComponent(flags['issue-state'] || 'open')}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
async function install() {
|
|
59
69
|
const config=configAt(state);
|
|
60
70
|
if (!['darwin','linux'].includes(process.platform)||!['arm64','x64'].includes(process.arch)) throw new Error('Use macOS/Linux arm64/amd64, or WSL2');
|
|
@@ -101,10 +111,10 @@ async function stop() {
|
|
|
101
111
|
}
|
|
102
112
|
stopContainers(state);console.log('Controller and its labelled containers stopped.');
|
|
103
113
|
}
|
|
104
|
-
async function submit(workflow,spec,sourceRef=flags['source-ref']) {
|
|
114
|
+
async function submit(workflow,spec,sourceRef=flags['source-ref'],sourceURL) {
|
|
105
115
|
if(Buffer.byteLength(spec)>240000)throw new Error('Task exceeds 240 KB');
|
|
106
116
|
const title=workflow==='defence'?'Private incident triage':spec.split('\n').find(s=>s.trim())?.replace(/^#+\s*/, '').slice(0,100)||'Software task';
|
|
107
|
-
return api(state,'/api/v1/jobs',{workflow,repository:'app',spec,title,...(sourceRef===undefined?{}:{source_ref:sourceRef})});
|
|
117
|
+
return api(state,'/api/v1/jobs',{workflow,repository:'app',spec,title,...(sourceURL?{source_url:sourceURL}:{}),...(sourceRef===undefined?{}:{source_ref:sourceRef})});
|
|
108
118
|
}
|
|
109
119
|
async function jobAction(action) {
|
|
110
120
|
const id=positional[0];if(!/^job_[a-z0-9]+$/.test(id || ''))throw new Error('A job ID is required');
|
|
@@ -157,6 +167,7 @@ async function abandonDeliveryJob(jobId) {
|
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
try {
|
|
170
|
+
if(flags['brief-file'] !== undefined && (command !== 'issue' || positional[0] !== 'start' || !(flags.url || flags.github) || flags.file || flags.draft))throw new Error('Use --brief-file only with issue start --url (or --github); local --file/--draft already supplies the scope.');
|
|
160
171
|
if(command==='init') {
|
|
161
172
|
if(!flags.repo)throw new Error('init requires --repo /path/to/existing/git/repo');
|
|
162
173
|
if(flags.harness && flags.agent && flags.harness !== flags.agent)throw new Error('--harness conflicts with legacy --agent');
|
|
@@ -191,9 +202,10 @@ try {
|
|
|
191
202
|
const value=command==='agents'?definition.agents:command==='skills'?{agents:definition.skills,operators:definition.operator_skills}:definition;
|
|
192
203
|
console.log(JSON.stringify(value,null,2));
|
|
193
204
|
}
|
|
194
|
-
else if(
|
|
205
|
+
else if(command==='inbox')console.log(JSON.stringify(await listInbox('inbox'),null,2));
|
|
206
|
+
else if(['infrastructure','automations'].includes(command)) {
|
|
195
207
|
const snapshot=await api(state,'/api/v1/status');
|
|
196
|
-
console.log(JSON.stringify(command==='
|
|
208
|
+
console.log(JSON.stringify(command==='automations'?snapshot.automation_control:snapshot[command],null,2));
|
|
197
209
|
}
|
|
198
210
|
else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
|
|
199
211
|
else if(command==='doctor') {
|
|
@@ -204,8 +216,7 @@ try {
|
|
|
204
216
|
} else if(command==='issue') {
|
|
205
217
|
const action=positional[0], sourceURL=flags.url || flags.github;
|
|
206
218
|
if(action==='list') {
|
|
207
|
-
|
|
208
|
-
console.log(JSON.stringify(['github','remote'].includes(flags.source) ? await api(state,`/api/v1/issues?page=${encodeURIComponent(flags.page || 1)}`) : (await api(state,'/api/v1/status')).jobs,null,2));
|
|
219
|
+
console.log(JSON.stringify(await listInbox('factory'),null,2));
|
|
209
220
|
} else if(action==='templates') console.log(JSON.stringify(await api(state,'/api/v1/issue-templates'),null,2));
|
|
210
221
|
else if(action==='connection') console.log(JSON.stringify(await api(state,'/api/v1/issue-connection'),null,2));
|
|
211
222
|
else if(action==='submissions') console.log(JSON.stringify(await api(state,'/api/v1/issue-submissions'),null,2));
|
|
@@ -233,17 +244,21 @@ try {
|
|
|
233
244
|
} else if(action==='start') {
|
|
234
245
|
if(!['software','defence'].includes(flags.workflow))throw new Error('Review the issue and choose --workflow software or defence');
|
|
235
246
|
if([flags.file,sourceURL,flags.draft].filter(Boolean).length!==1)throw new Error('Choose --file brief.md, --draft draft.json or --url ISSUE_URL');
|
|
236
|
-
let input;
|
|
237
|
-
if(
|
|
247
|
+
let input, brief;
|
|
248
|
+
if(flags['brief-file'] !== undefined) {
|
|
249
|
+
brief=readFileSync(resolve(flags['brief-file']),'utf8');
|
|
250
|
+
if(brief.length>16000)throw new Error('Operator brief must be at most 16000 characters.');
|
|
251
|
+
}
|
|
252
|
+
if(sourceURL) { const issue=await api(state,'/api/v1/issues/preview',{url:sourceURL});input={title:issue.title,url:issue.url,expected_spec:issue.spec,...(brief===undefined?{}:{brief})}; }
|
|
238
253
|
else if(flags.draft) { const draft=json(resolve(flags.draft));input={title:draft.title,spec:draft.spec}; }
|
|
239
254
|
else input={title:flags.title,spec:readFileSync(resolve(flags.file),'utf8')};
|
|
240
255
|
input.title=flags.title || input.title;
|
|
241
|
-
if(typeof input.title!=='string'||!input.title.trim()||input.title.length>160)throw new Error('Provide a title of 1–160 characters (use --title for a blank issue)');
|
|
256
|
+
if(!sourceURL && (typeof input.title!=='string'||!input.title.trim()||input.title.length>160))throw new Error('Provide a title of 1–160 characters (use --title for a blank issue)');
|
|
242
257
|
if(flags.workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
|
|
243
|
-
console.log(JSON.stringify(await api(state,'/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || '',...(flags['source-ref']===undefined?{}:{source_ref:flags['source-ref']})}),null,2));
|
|
258
|
+
console.log(JSON.stringify(await api(state,sourceURL ? '/api/v1/issues/start' : '/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || '',...(flags['source-ref']===undefined?{}:{source_ref:flags['source-ref']})}),null,2));
|
|
244
259
|
} else throw new Error('Use issue list|connection|templates|preview|recommend|draft|create|start|submissions|recover; see help');
|
|
245
260
|
} else if(command==='issues') {
|
|
246
|
-
console.log(JSON.stringify(await
|
|
261
|
+
console.log(JSON.stringify(await listInbox('inbox'),null,2));
|
|
247
262
|
} else if(command==='recommend') {
|
|
248
263
|
if(Boolean(flags.issue) === Boolean(flags.file))throw new Error('Choose --file task.md or --issue URL');
|
|
249
264
|
const recommendation=flags.issue ? (await readIssue(configAt(state).repo,flags.issue)).recommendation : recommendWork({spec:readFileSync(resolve(flags.file),'utf8')});
|
|
@@ -251,13 +266,13 @@ try {
|
|
|
251
266
|
} else if(command==='run') {
|
|
252
267
|
const workflow=flags.workflow || 'software';
|
|
253
268
|
if(!['software','defence'].includes(workflow))throw new Error('Choose --workflow software or defence');
|
|
254
|
-
let spec;
|
|
269
|
+
let spec,sourceURL;
|
|
255
270
|
if(flags.issue) {
|
|
256
|
-
|
|
271
|
+
const issue=await readIssue(configAt(state).repo,flags.issue);spec=issue.spec;sourceURL=issue.url;
|
|
257
272
|
} else if(flags.file)spec=readFileSync(resolve(flags.file),'utf8');
|
|
258
273
|
else throw new Error('Use --file task.md or --issue https://github.com/owner/repo/issues/123');
|
|
259
274
|
if(workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
|
|
260
|
-
console.log(JSON.stringify(await submit(workflow,spec)));
|
|
275
|
+
console.log(JSON.stringify(await submit(workflow,spec,flags['source-ref'],sourceURL)));
|
|
261
276
|
} else if(command==='incident') {
|
|
262
277
|
if(!flags.file)throw new Error('Use --file incident.json; see factory/examples/incident.json');
|
|
263
278
|
console.log(JSON.stringify(await admitIncident(state,json(resolve(flags.file)),submit)));
|
|
@@ -303,7 +318,9 @@ try {
|
|
|
303
318
|
web probe --state PATH Execute the pinned local Chromium readiness probe
|
|
304
319
|
foundation Read the operator setup skill; no installation required
|
|
305
320
|
definition | agents | skills Inspect roles, instructions and installation settings
|
|
306
|
-
inbox
|
|
321
|
+
inbox [--page N] [--issue-state open|closed|all] [--source inbox|factory]
|
|
322
|
+
Repository backlog (default); factory: execution-only array
|
|
323
|
+
infrastructure | automations Inspect host/worker and automation state
|
|
307
324
|
workflows Compatibility alias for definition
|
|
308
325
|
--agent Legacy alias for init --harness
|
|
309
326
|
serve Foreground supervisor
|
|
@@ -316,7 +333,8 @@ try {
|
|
|
316
333
|
service resume Release a reconciled maintenance reservation
|
|
317
334
|
tunnel install|start|stop|status|logs|uninstall --host SSH_ALIAS --port PORT
|
|
318
335
|
Persistent loopback SSH tunnel (macOS/Linux)
|
|
319
|
-
issue list [--source remote] [--page N]
|
|
336
|
+
issue list [--source inbox|remote|factory] [--page N] [--issue-state open|closed|all]
|
|
337
|
+
List linked repository issues or local executions (default)
|
|
320
338
|
issue templates Read this repository's issue forms and contact links
|
|
321
339
|
issue preview --url URL Preview one repository issue without starting work
|
|
322
340
|
issue recommend --file brief.md | --url URL
|
|
@@ -328,8 +346,9 @@ try {
|
|
|
328
346
|
issue recover --key REQUEST_ID Reconcile an uncertain creation without another write
|
|
329
347
|
issue start --draft draft.json | --url URL | --file brief.md --title TITLE
|
|
330
348
|
--workflow software|defence [--source-ref REF] [--model MODEL]
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
[--brief-file operator.md] Only with --url; at most 16000 characters
|
|
350
|
+
Explicitly start execution; no GitHub write
|
|
351
|
+
issues [--page N] Browse linked project issues; supports --issue-state
|
|
333
352
|
recommend --file task.md | --issue URL Suggest a work type without starting work
|
|
334
353
|
run --file task.md | --issue URL Submit software (default), or --workflow defence
|
|
335
354
|
[--source-ref REF] Pin a configured-repository ref before admission
|
package/docs/interfaces.md
CHANGED
|
@@ -9,9 +9,10 @@ shell endpoint or a second scheduler.
|
|
|
9
9
|
|
|
10
10
|
| Capability | CLI | Shared API | Dashboard | Remaining work |
|
|
11
11
|
| --- | --- | --- | --- | --- |
|
|
12
|
-
| Project/queue/attempt state | `status`, `inbox` JSON | `GET /api/v1/status` | Project, tasks, details/history | Stable versioned agent result/error contract |
|
|
13
|
-
| Start local work | `issue start --file --title
|
|
14
|
-
| Browse
|
|
12
|
+
| Project/queue/attempt state | `status`, `inbox --source factory` JSON | `GET /api/v1/status` | Project, tasks, details/history | Stable versioned agent result/error contract |
|
|
13
|
+
| Start local work | `issue start --file --title` or `--draft`, explicit `--workflow`, optional `--model` | `POST /api/v1/jobs` | Local execution only → review → Create & start locally | Persistent unstarted drafts and typed incident intake remain separate |
|
|
14
|
+
| Browse repository Inbox | `inbox [--page N] [--issue-state open/closed/all]` (also `issue list --source inbox`), `issue preview --url URL` via controller provider | Authenticated `GET /api/v1/issues`, `POST /api/v1/issues/preview` using shared readers | Inbox with provider/state/readiness, loaded-page counts, linked attempts, local/off-page history; explicit Start work for either type | Issue → execution links retained; no implicit polling |
|
|
15
|
+
| Start repository work | `issue start --url URL --workflow software/defence [--brief-file operator.md]` | `POST /api/v1/issues/start` | Issue context → explicit Start work with operator brief | Rechecks current content and active admission |
|
|
15
16
|
| Create repository issue / recovery | `issue connection`, `create --key`, `submissions`, `recover --key` | Authenticated connection, `POST /issues`, receipts and recovery | Display destination/actor, create without execution, recover uncertain result | GitHub adapter first; assignees/projects and other providers unimplemented |
|
|
16
17
|
| Repository issue templates | `issue templates`, `issue draft --template --sha --file` | Authenticated template list and draft compilation | Chooser, fields/defaults/validation, review | Supports Markdown and YAML markdown/input/textarea/dropdown/checkboxes; unsupported templates link to GitHub |
|
|
17
18
|
| Suggest task type | `issue recommend --file` or `--url` | Authenticated `POST /api/v1/intake/recommend`; issue preview includes suggestion | Editable recommendation after source selection | Deterministic label/brief rules; no model judgment or execution authority |
|
|
@@ -32,7 +33,7 @@ shell endpoint or a second scheduler.
|
|
|
32
33
|
| SSH tunnels | `tunnel` | No tunnel endpoint | None | Client-host ownership; distinguish operator machine from worker |
|
|
33
34
|
| Method export | `kit --output` | No export endpoint | None | Equivalent download/export preserving staging-only adoption |
|
|
34
35
|
| Synthetic qualification | `demo`, `qualify` | No qualification endpoint | Synthetic disclosure only | Explicit separate state; never target an application accidentally |
|
|
35
|
-
| Immutable source admission | `init --source-ref`, `run --source-ref`, `issue start --source-ref`; status and build evidence carry the resolved SHA | `POST /api/v1/jobs` resolves/retains before acknowledgement; shared source metadata in status |
|
|
36
|
+
| Immutable source admission | `init --source-ref`, `run --source-ref`, `issue start --source-ref`; status and build evidence carry the resolved SHA | `POST /api/v1/jobs` resolves/retains before acknowledgement; shared source metadata in status | Issue Start work, local request and revision forms accept a ref; task detail shows requested ref, resolved SHA and prior source commits | Build/retry use retained objects; revisions start fresh by default; explicit continuation keeps the reviewed tree and recorded source; a new ref replaces the base; legacy source remains unknown |
|
|
36
37
|
| Trusted PR handoff | `publish JOB_ID` publishes/reconciles; `abandon-delivery JOB_ID --branch-sha SHA` records a checked local resolution for a pre-write branch collision | Authenticated `POST /api/v1/jobs/:id/publish` and `/abandon-delivery`; shared receipt, conflict identity and removal policy | Publish/reconcile and explicit “Abandon local delivery; keep remote branch” actions share controller state; errors/results and inspected branch identity are visible | New writes require matching protected Codex/Pi build/review provenance, deterministic verify/handoff provenance, non-synthetic bound artifacts and a qualified GitHub Actions tree. Shared delivery status exposes `workflow_qualification` and the same reason blocks CLI/API/dashboard capability and publication/retry. Candidate workflow changes, unsupported triggers/syntax, or active generated-push, selected-ref-dispatch and PR jobs with write/secrets/environment/OIDC/deploy access, self-hosted runners or ambiguous privileged guards refuse trusted writes. Supported ASCII guard comparisons follow GitHub's case-insensitive string semantics; unknown PR refs, non-ASCII mismatches, and glob/escaped branch filters cannot prove a privileged job inactive. The shared summary's `action_mode` distinguishes new/resumable publication from read-only reconciliation and drives idle and pending task button wording. Branch-only collisions and unknown/abandoned states offer neither; known PR receipts and pending PR-creation checkpoints retain read-only reconciliation. Abandonment checks the current run, saved intent, exact branch head and absence of an associated PR; it writes no provider data, preserves the remote branch/evidence, disables republishing and permits local removal. Uncertain effects and incompatible evidence stay blocked. Destination remains private operator config; patch-only remains default |
|
|
37
38
|
| Optional trusted web verification | `web probe` performs a real local Chromium interaction; `doctor` reports readiness | Verify stores a shared story summary and protected JSON artifact in the run | Task history shows passed/failed/unavailable/inconclusive plus tool, candidate, policy and story hashes | Disabled by default. Required operator stories and Playwright/Chromium image ID are frozen in attempt policy. Linux Chromium proof cannot qualify native/mobile OS behavior; see [the browser contract](web-verification.md) |
|
|
38
39
|
|
|
@@ -57,3 +58,51 @@ the Skills page reads that same file. Full safe setup controls remain #37.
|
|
|
57
58
|
The roadmap is split into Defence #50, quality measurement #51, GitHub intake
|
|
58
59
|
#52, editable definitions #53 and scoped MCP #54. Existing REST endpoints are
|
|
59
60
|
local single-operator interfaces, not a public multi-user API.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Issue lifecycle API in 0.11.0
|
|
64
|
+
|
|
65
|
+
`GET /api/v1/issues?page=1&state=open` retains repository/issues/next_page and adds
|
|
66
|
+
provider, page, state, loaded_count, total (null when unknown), and history.
|
|
67
|
+
Each issue adds canonical identity, provider state, readiness, executions,
|
|
68
|
+
latest_execution, active_execution(s), and start_block_reason. History groups
|
|
69
|
+
local jobs and canonical sources outside the page; not_loaded does not claim
|
|
70
|
+
that an issue is missing or closed. `status.issue_history` refreshes these local
|
|
71
|
+
associations without polling the provider. No issue content database was added.
|
|
72
|
+
|
|
73
|
+
`POST /api/v1/issues/preview` retains content/labels/recommendation and adds the
|
|
74
|
+
same identity/readiness/history contract. `POST /api/v1/issues/start` accepts
|
|
75
|
+
url, expected_spec (the preview's spec), workflow, optional brief, source_ref
|
|
76
|
+
and model. It re-reads current provider context, rejects changed scope, closed
|
|
77
|
+
sources, blocked/conflicting readiness and active/unresolved work, then uses
|
|
78
|
+
existing source admission. HTTP 409 describes stale or duplicate admission.
|
|
79
|
+
`POST /api/v1/jobs` remains compatible for local/direct callers; its queue-level
|
|
80
|
+
canonical reservation also rejects concurrent duplicate active work and retries.
|
|
81
|
+
Terminal succeeded/failed/cancelled executions release the reservation unless
|
|
82
|
+
provider delivery is unresolved. Other/unknown states retain it conservatively.
|
|
83
|
+
|
|
84
|
+
`inbox` now defaults to the same repository page object as the dashboard (open,
|
|
85
|
+
page 1), an intentional 0.11.0 JSON change from its former jobs array. Use
|
|
86
|
+
`inbox --source factory` for the explicit legacy execution-only array.
|
|
87
|
+
`issue list` still defaults to the original local jobs array (`--source factory`).
|
|
88
|
+
`--source inbox`, `remote` and compatibility `github` return the shared enriched
|
|
89
|
+
page. `issues` now uses this same authenticated controller endpoint and enriched
|
|
90
|
+
JSON, rather than bypassing the controller; scripts need a running controller.
|
|
91
|
+
Use `--issue-state` for provider state; `--state` continues to mean installation
|
|
92
|
+
path. Repository paging/state options are rejected in execution-only mode rather
|
|
93
|
+
than ignored. Unsupported providers return their capability state and local/history
|
|
94
|
+
records; provider/auth failures exit nonzero, never an empty-success backlog.
|
|
95
|
+
`issue start --url URL --brief-file operator.md` reads a UTF-8 operator brief of at
|
|
96
|
+
most 16000 characters (the API's string-length limit), forwarded unchanged to the
|
|
97
|
+
shared preview/start contract. The API appends nonblank, trimmed text under
|
|
98
|
+
`Operator brief:` in the admitted spec, while preserving provider identity and
|
|
99
|
+
rechecking current content. `--brief-file` is optional, only valid with remote
|
|
100
|
+
`issue start --url` (or its `--github` alias); it cannot accompany local `--file`
|
|
101
|
+
or `--draft`, creation, browsing or other commands. Missing/unreadable files and
|
|
102
|
+
oversize text fail without admission. Local file/draft execution is unchanged. `run` retains
|
|
103
|
+
its compatible direct-job path and the controller's duplicate identity guard.
|
|
104
|
+
|
|
105
|
+
Definition exposes `configuration.issueReadinessLabels`. The optional private
|
|
106
|
+
config field has exactly triage/spec/ready/blocked keys with four distinct label
|
|
107
|
+
names; defaults are factory:triage/spec/ready/blocked. Configuration is validated,
|
|
108
|
+
not inferred from issue content. No browsing path changes labels or comments.
|
package/docs/quickstart.md
CHANGED
|
@@ -149,7 +149,7 @@ create a second PR or overwrite a changed branch. The CLI gives this bounded
|
|
|
149
149
|
multi-request action ten minutes; branch resolution uses the same bound and
|
|
150
150
|
other API calls keep their five-second deadline. If a client deadline expires,
|
|
151
151
|
inspect status and repeat `publish` to reconcile or recheck the reported branch
|
|
152
|
-
identity before `abandon-delivery`.
|
|
152
|
+
identity before `abandon-delivery`. Remove local execution history stays disabled while delivery
|
|
153
153
|
is unresolved, and the controller enforces the same guard on its API.
|
|
154
154
|
Publication does not merge, integrate, release or deploy. See [delivery
|
|
155
155
|
recovery](recovery.md#trusted-pr-delivery).
|
|
@@ -211,8 +211,7 @@ use the [Defence integration](defence-integration.md) recipe; the generic
|
|
|
211
211
|
Defence form is not that typed intake path.
|
|
212
212
|
|
|
213
213
|
The header names the configured project. View repo opens a validated GitHub
|
|
214
|
-
origin. New issue opens
|
|
215
|
-
form or existing GitHub issues. Create issue saves to the supported repository provider without execution. Start work queues local work. See [intake and CLI examples](workflows.md). The task detail provides previous/next within the filtered list, copy
|
|
214
|
+
origin. Inbox lists repository issues directly with open/closed/all filters and paging. New issue opens repository templates or a blank creation form. Create issue saves to the supported repository provider without execution. Open an issue from Inbox and choose Start work to admit execution explicitly. The controller rejects duplicate active work; linked attempts remain accessible. See [intake and CLI examples](workflows.md). The task detail provides previous/next within the filtered list, copy
|
|
216
215
|
link and close (Escape). Closing preserves the list's filters and position.
|
|
217
216
|
|
|
218
217
|
If the interface looks unexpectedly small, check the browser zoom. The design
|
package/docs/setup.md
CHANGED
|
@@ -227,7 +227,7 @@ controller action; other CLI API requests retain their five-second deadline.
|
|
|
227
227
|
If the client deadline expires, inspect status and repeat `publish JOB_ID` so
|
|
228
228
|
the controller can reconcile its saved intent. Restart the installed controller
|
|
229
229
|
and repeat `publish JOB_ID`; verify the same branch and PR head are read back
|
|
230
|
-
and no second PR appears.
|
|
230
|
+
and no second PR appears. Remove local execution history is disabled while the delivery is
|
|
231
231
|
unresolved, and the controller rejects the same removal through its API. Leave
|
|
232
232
|
pending/unknown checks labelled as such. After inspection, close the proof PR
|
|
233
233
|
without merging its fixture change. Do not publish a worker candidate from
|
|
@@ -326,3 +326,36 @@ installed file. Keep host identities, credentials, raw logs and customer details
|
|
|
326
326
|
out of public issues and package contents. A ready worker is only the foundation:
|
|
327
327
|
follow [the method](../kit/README.md#first-real-task) for the first explicitly
|
|
328
328
|
accepted application task and revision-bound checks/review/handoff.
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
## Repository Inbox and explicit execution
|
|
332
|
+
|
|
333
|
+
The controller's configured repository selects its issue adapter. On GitHub,
|
|
334
|
+
use the controller host's existing read access; browser login does not supply
|
|
335
|
+
credentials. Open Inbox, verify the provider/repository, refresh and page through
|
|
336
|
+
Open/Closed/All states. Provider failures remain visible; local execution and
|
|
337
|
+
retained history remain available on unsupported hosts. Creating through New
|
|
338
|
+
issue and browsing must leave the execution queue unchanged. Open the issue and
|
|
339
|
+
choose Start work only after reviewing its scope and work type.
|
|
340
|
+
|
|
341
|
+
Readiness is separate from execution state. To use different repository labels,
|
|
342
|
+
set `issueReadinessLabels` in private `factory.json` while stopped and restart:
|
|
343
|
+
`{"triage":"factory:triage","spec":"factory:spec","ready":"factory:ready","blocked":"factory:blocked"}`.
|
|
344
|
+
All four values must be distinct label names. Definition displays the effective
|
|
345
|
+
mapping. This only interprets read metadata; it installs no labels or automations.
|
|
346
|
+
Keep private security reports on their configured private route.
|
|
347
|
+
|
|
348
|
+
Before adopting 0.11.0, qualify the exact installed package and real provider
|
|
349
|
+
lifecycle, including creation without execution, explicit start, duplicate
|
|
350
|
+
rejection and linked subsequent attempts. Inspect affected flows at desktop,
|
|
351
|
+
390px and 320px in both themes, including failures. Component/provider-fixture
|
|
352
|
+
tests do not qualify those native interactions. Existing source retention,
|
|
353
|
+
continuation, review and trusted delivery acceptance remain required.
|
|
354
|
+
|
|
355
|
+
CLI `inbox --state PATH` opens the same repository page (open issues, page 1).
|
|
356
|
+
Use `--page N` and `--issue-state closed|all` for additional issues/history;
|
|
357
|
+
`inbox --source factory` explicitly selects the legacy execution-only array.
|
|
358
|
+
To add operator scope at admission, use `issue start --url URL --workflow software
|
|
359
|
+
--brief-file operator.md --state PATH` with an optional UTF-8 brief of at most
|
|
360
|
+
16000 characters. This keeps the remote identity and current-content check;
|
|
361
|
+
local requests still use `issue start --file` or `--draft` without `--brief-file`.
|
package/docs/workflows.md
CHANGED
|
@@ -24,12 +24,27 @@ kept outside those execution mounts.
|
|
|
24
24
|
|
|
25
25
|
## Start work
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
Inbox lists repository issues directly. Choose Open, Closed or All states and use
|
|
28
|
+
Previous/Next page or Refresh issues. Search covers the loaded page and visible
|
|
29
|
+
history, not the whole repository. GitHub returns up to 50 records per page;
|
|
30
|
+
pull requests are excluded, so even a page with zero issues can have a next page.
|
|
31
|
+
Counts name the loaded page and total remains unknown. Authentication/provider
|
|
32
|
+
failures are visible, with any retained page explicitly stale.
|
|
33
|
+
|
|
34
|
+
Open an issue for its context, readiness, and linked execution attempts. Start
|
|
35
|
+
work is deliberate; browsing never starts an agent. Active or unresolved work
|
|
36
|
+
blocks another admission for the same canonical identity, including concurrent
|
|
37
|
+
requests and retries. Completion/cancellation permits a subsequent explicit
|
|
38
|
+
attempt. Failed work stays in history; blocked/interrupted work must first be
|
|
39
|
+
reconciled or cancelled, and unresolved provider delivery continues to block.
|
|
40
|
+
Closed issues and blocked/conflicting readiness labels cannot start new work.
|
|
41
|
+
No readiness labels means unknown, not ready or running. Readiness is planning
|
|
42
|
+
metadata; Needs triage never means an agent is Triaging.
|
|
43
|
+
|
|
44
|
+
**New issue** only composes/publishes: choose a repository template (or **Blank
|
|
45
|
+
issue**), complete the title and fields, then Continue and Create issue. Done
|
|
46
|
+
returns to Inbox, where the new issue appears without a job. Refresh also finds
|
|
47
|
+
issues created directly on the forge. Existing issue selection is in Inbox.
|
|
33
48
|
|
|
34
49
|
Review the instructions and suggested work type before explicitly starting work. The shared,
|
|
35
50
|
deterministic suggestion prioritizes `track:software` and `track:security` (also
|
|
@@ -57,8 +72,8 @@ shown as literal text, never executed or rendered as raw HTML.
|
|
|
57
72
|
On a supported repository, **Create issue on GitHub** writes the title, description
|
|
58
73
|
and template labels to that repository using the displayed host identity. It
|
|
59
74
|
returns the real issue number/link and **does not start execution**. Select
|
|
60
|
-
**
|
|
61
|
-
Choose **Local execution
|
|
75
|
+
**Done**, then open the issue in Inbox and choose **Start work**.
|
|
76
|
+
Choose **Local execution request** in Inbox to submit a brief without publishing it. On unsupported hosts, New issue also offers this explicitly local route. A local
|
|
62
77
|
brief is an execution request; an unfinished form is not a persistent backlog.
|
|
63
78
|
Use the private security contact route for sensitive reports, never a public issue.
|
|
64
79
|
|
|
@@ -73,17 +88,24 @@ CLI equivalents (the selected controller must be running):
|
|
|
73
88
|
|
|
74
89
|
```sh
|
|
75
90
|
software-defence-factory issue connection --state PATH
|
|
76
|
-
software-defence-factory
|
|
91
|
+
software-defence-factory inbox --state PATH --page 1 --issue-state open
|
|
92
|
+
# Explicit legacy execution-only JSON array:
|
|
93
|
+
software-defence-factory inbox --state PATH --source factory
|
|
77
94
|
software-defence-factory issue templates --state PATH
|
|
78
95
|
software-defence-factory issue draft --state PATH --template bug-report.yml --sha TEMPLATE_SHA --file answers.json > draft.json
|
|
79
96
|
software-defence-factory issue create --state PATH --draft draft.json --key release-board-fix-01
|
|
80
97
|
software-defence-factory issue submissions --state PATH
|
|
81
98
|
software-defence-factory issue recover --state PATH --key release-board-fix-01
|
|
82
99
|
# Explicit execution, independent of creation:
|
|
83
|
-
software-defence-factory issue start --state PATH --url URL --workflow software
|
|
100
|
+
software-defence-factory issue start --state PATH --url URL --workflow software --brief-file operator.md
|
|
84
101
|
software-defence-factory issue start --state PATH --file brief.md --title "Investigate supplied evidence" --workflow defence --source-ref main
|
|
85
102
|
```
|
|
86
103
|
|
|
104
|
+
`--brief-file` is optional, UTF-8, at most 16000 characters and valid only with
|
|
105
|
+
a remote issue start. Use `--file` or `--draft` alone for local scope. Inbox
|
|
106
|
+
defaults to a repository page with linked history; `--issue-state closed` or
|
|
107
|
+
`all` and `--page` browse further without starting work.
|
|
108
|
+
|
|
87
109
|
`answers.json` contains `{"title":"Fix the board","answers":{"problem":"..."}}`;
|
|
88
110
|
keys match `fields[].id` in `issue templates`. Multi-select/checkbox answers are
|
|
89
111
|
arrays of exact option labels. `issue create` now publishes only; migrate 0.5.1
|
|
@@ -116,3 +138,12 @@ while the installation is stopped, then restart. Workflow order and packaged
|
|
|
116
138
|
skills change through reviewed Factory releases. This release does not support
|
|
117
139
|
per-role profiles or arbitrary editable workflow graphs. Versioned editable
|
|
118
140
|
definitions are tracked in [#53](https://github.com/arcitai/software-and-defence-factory/issues/53).
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
Inbox groups URL case, HTTP/HTTPS, trailing-slash, query and fragment aliases by GitHub
|
|
144
|
+
repository and issue number. Credential-bearing URLs, queries, foreign hosts and
|
|
145
|
+
non-issue paths are not admitted by the provider preview. Local-only records and
|
|
146
|
+
executions whose source is missing, closed or outside the loaded page remain in
|
|
147
|
+
Local and other execution history. Removing local execution history preserves
|
|
148
|
+
private evidence and never deletes a provider issue; unresolved delivery guards
|
|
149
|
+
still apply. The separate Execution history tab keeps the execution list/board.
|
package/factory/definition.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readinessMapping } from './issue-lifecycle.mjs';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { ROOT, digest, harnessOf } from './lib.mjs';
|
|
@@ -39,7 +40,7 @@ export function factoryDefinition(config) {
|
|
|
39
40
|
commands: Object.entries(phaseInfo).map(([name, info]) => ({ name, ...info, prompt: info.description,
|
|
40
41
|
executor: info.owner === 'agent' ? harnessOf(config) : 'factory', timeout: `${config.timeoutSeconds}s` })),
|
|
41
42
|
skills,
|
|
42
|
-
configuration: { harness, agent: harness, // agent is a v1 compatibility alias
|
|
43
|
+
configuration: { issueReadinessLabels: readinessMapping(config.issueReadinessLabels), harness, agent: harness, // agent is a v1 compatibility alias
|
|
43
44
|
model: config.model || null, check: config.check, timeoutSeconds: config.timeoutSeconds,
|
|
44
45
|
memoryMiB: config.memoryMiB, cpus: config.cpus || 2,
|
|
45
46
|
web_verification: config.webVerification?.enabled ? {
|
|
@@ -7,11 +7,11 @@ import { effectiveInferenceProvider } from './model-environment.mjs';
|
|
|
7
7
|
import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
8
8
|
|
|
9
9
|
// Audited v1 protected evidence writers: 0.8.0 (380f749), 0.9.0 (cdaadef),
|
|
10
|
-
// 0.9.1 and 0.
|
|
10
|
+
// 0.9.1, 0.10.0 and 0.11.0 (unchanged protected evidence writers). Deliberately independent of VERSION: a release bump is not
|
|
11
11
|
// evidence compatibility. Re-audit this list for every trust-relevant writer,
|
|
12
12
|
// isolation or validation change; remove versions whose guarantees no longer
|
|
13
13
|
// satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
|
|
14
|
-
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0']);
|
|
14
|
+
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0']);
|
|
15
15
|
|
|
16
16
|
export function isSupportedExecutionProfile(profile) {
|
|
17
17
|
return profile?.version === 1 && SUPPORTED_EXECUTION_RUNTIMES_V1.has(profile.runtimeVersion);
|
package/factory/issue-intake.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalIssue } from './issue-lifecycle.mjs';
|
|
1
2
|
import { execFile } from 'node:child_process';
|
|
2
3
|
import { promisify } from 'node:util';
|
|
3
4
|
import { readProjectLinks } from './project-links.mjs';
|
|
@@ -23,34 +24,36 @@ function issueLabels(labels = []) {
|
|
|
23
24
|
return labels.map(label => ({name:label.name, color:/^[a-f0-9]{6}$/i.test(label.color || '') ? label.color.toLowerCase() : null}));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export async function listIssues(repo, page = 1, read = githubRead) {
|
|
27
|
+
export async function listIssues(repo, page = 1, read = githubRead, state = 'open') {
|
|
27
28
|
if (!Number.isSafeInteger(page) || page < 1 || page > 10000) throw new Error('Issue page must be an integer between 1 and 10000.');
|
|
29
|
+
if (!['open', 'closed', 'all'].includes(state)) throw new Error('Issue state must be open, closed or all.');
|
|
28
30
|
const repository = readProjectLinks(repo)?.repository;
|
|
29
31
|
if (!repository) throw new Error('This project has no configured GitHub origin.');
|
|
30
32
|
const slug = repository.slice('https://github.com/'.length);
|
|
31
|
-
const result = await read(['api', '--hostname', 'github.com', `repos/${slug}/issues?state
|
|
33
|
+
const result = await read(['api', '--hostname', 'github.com', `repos/${slug}/issues?state=${state}&sort=created&direction=desc&per_page=50&page=${page}`, '-H', 'Accept: application/vnd.github+json']);
|
|
32
34
|
if (!Array.isArray(result) || result.length > 50) throw new Error('GitHub returned an unexpected issue list.');
|
|
33
35
|
const issues = result.filter(issue => !issue.pull_request).map(issue => {
|
|
34
36
|
validateIssueURL(repository, issue.html_url);
|
|
35
37
|
if (!Number.isSafeInteger(issue.number) || !issue.html_url.endsWith(`/issues/${issue.number}`) || typeof issue.title !== 'string' || !issue.title.trim()) throw new Error('GitHub returned an unexpected issue.');
|
|
36
|
-
return { number: issue.number, title: issue.title, url: issue.html_url, labels: issueLabels(issue.labels) };
|
|
38
|
+
return { number: issue.number, title: issue.title, url: issue.html_url, state: issue.state || 'unknown', labels: issueLabels(issue.labels) };
|
|
37
39
|
});
|
|
38
40
|
return { repository, issues, next_page: result.length === 50 && page < 10000 ? page + 1 : null };
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export function validateIssueURL(repoURL, value) {
|
|
42
|
-
|
|
43
|
-
if (!
|
|
44
|
-
|
|
44
|
+
const identity = canonicalIssue(value);
|
|
45
|
+
if (!identity || new URL(value).search) throw new Error('Enter a GitHub issue URL without query parameters.');
|
|
46
|
+
if (!repoURL || identity.repository !== repoURL.toLowerCase()) throw new Error('Issue does not belong to this project’s configured GitHub origin.');
|
|
47
|
+
return identity.url;
|
|
45
48
|
}
|
|
46
|
-
export async function readIssue(repo, url, read = url => githubRead(['issue', 'view', url, '--json', 'title,body,url,labels'])) {
|
|
49
|
+
export async function readIssue(repo, url, read = url => githubRead(['issue', 'view', url, '--json', 'title,body,url,labels,state'])) {
|
|
47
50
|
const repoURL = readProjectLinks(repo)?.repository;
|
|
48
|
-
validateIssueURL(repoURL, url);
|
|
51
|
+
url = validateIssueURL(repoURL, url);
|
|
49
52
|
const issue = await read(url);
|
|
50
53
|
validateIssueURL(repoURL, issue?.url);
|
|
51
|
-
if (issue.url
|
|
54
|
+
if (validateIssueURL(repoURL, issue.url) !== url || typeof issue.title !== 'string' || !issue.title.trim() || typeof issue.body !== 'string') throw new Error('GitHub returned an unexpected issue.');
|
|
52
55
|
const spec = `Issue: ${issue.url}\n${issue.title}\n\n${issue.body}`;
|
|
53
56
|
if (Buffer.byteLength(spec) > 240000) throw new Error('Issue exceeds the 240 KB task limit. Use a bounded task file instead.');
|
|
54
57
|
const labels = issueLabels(issue.labels);
|
|
55
|
-
return { title: issue.title, url: issue.
|
|
58
|
+
return { title: issue.title, url, number: canonicalIssue(url).number, state: issue.state?.toLowerCase() || 'unknown', body: issue.body, spec, labels, recommendation: recommendWork({ spec, labels: labels.map(label => label.name) }) };
|
|
56
59
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Provider issue truth is read-only here. Only execution associations come from SQLite.
|
|
2
|
+
export const DEFAULT_READINESS_LABELS = Object.freeze({ triage: 'factory:triage', spec: 'factory:spec', ready: 'factory:ready', blocked: 'factory:blocked' });
|
|
3
|
+
export function readinessMapping(value = DEFAULT_READINESS_LABELS) {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
5
|
+
|| Object.keys(value).some(key => !Object.hasOwn(DEFAULT_READINESS_LABELS, key))
|
|
6
|
+
|| Object.keys(DEFAULT_READINESS_LABELS).some(key => typeof value[key] !== 'string' || !value[key].trim() || value[key].length > 100)
|
|
7
|
+
|| new Set(Object.values(value).map(label => label.toLowerCase())).size !== 4)
|
|
8
|
+
throw new Error('issueReadinessLabels must map triage, spec, ready and blocked to four distinct label names.');
|
|
9
|
+
return { ...value };
|
|
10
|
+
}
|
|
11
|
+
export function issueReadiness(labels = [], mapping = DEFAULT_READINESS_LABELS) {
|
|
12
|
+
const names = labels.map(label => (typeof label === 'string' ? label : label.name).toLowerCase());
|
|
13
|
+
const matches = Object.entries(mapping).filter(([, label]) => names.includes(label.toLowerCase())).map(([key]) => key);
|
|
14
|
+
const state = matches.length > 1 ? 'conflicting' : matches[0] || 'unknown';
|
|
15
|
+
return { state, label: { triage:'Needs triage', spec:'Needs specification', ready:'Ready', blocked:'Blocked', conflicting:'Conflicting readiness labels', unknown:'Readiness unknown' }[state] };
|
|
16
|
+
}
|
|
17
|
+
export function canonicalIssue(value) {
|
|
18
|
+
if (typeof value !== 'string' || value.length > 2048 || /(?:^|\/)\.\.?(?:\/|$)/.test(value)) return null;
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(value);
|
|
21
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.hostname !== 'github.com' || url.username || url.password || url.port) return null;
|
|
22
|
+
const match = url.pathname.match(/^\/([A-Za-z0-9-]+)\/([A-Za-z0-9_.-]+)\/issues\/([1-9][0-9]*)\/?$/i);
|
|
23
|
+
if (!match || !Number.isSafeInteger(Number(match[3]))) return null;
|
|
24
|
+
const repository = `https://github.com/${match[1]}/${match[2]}`.toLowerCase(), number = Number(match[3]);
|
|
25
|
+
return { key:`github:${repository}:${number}`, provider:'github', repository, number, url:`${repository}/issues/${number}` };
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
export function executionReservesIssue(job) {
|
|
29
|
+
return !job.deleted_at && (!['succeeded', 'failed', 'cancelled'].includes(job.state)
|
|
30
|
+
|| Boolean(job.delivery && !['published', 'abandoned'].includes(job.delivery.state)));
|
|
31
|
+
}
|
|
32
|
+
export function executionAssociation(jobs, identity) {
|
|
33
|
+
const attempts = jobs.filter(job => canonicalIssue(job.task?.source_url)?.key === identity.key)
|
|
34
|
+
.sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')) || b.id.localeCompare(a.id));
|
|
35
|
+
const describe = job => ({ id:job.id, state:job.state, workflow:job.workflow?.name || null, phase:job.workflow?.steps?.[job.workflow.current_step] || null, created_at:job.created_at, updated_at:job.updated_at });
|
|
36
|
+
const active = attempts.filter(executionReservesIssue).map(describe);
|
|
37
|
+
return { executions:attempts.map(describe), latest_execution:attempts[0] ? describe(attempts[0]) : null, active_execution:active[0] || null, active_executions:active };
|
|
38
|
+
}
|
|
39
|
+
export function associateIssue(issue, jobs, mapping) {
|
|
40
|
+
const identity = canonicalIssue(issue.url);
|
|
41
|
+
if (!identity) throw new Error('Provider returned an unsupported issue identity.');
|
|
42
|
+
const association = executionAssociation(jobs, identity);
|
|
43
|
+
const readiness = issueReadiness(issue.labels, mapping);
|
|
44
|
+
const start_block_reason = association.active_execution ? 'An execution is active or unresolved. Open its history to continue or cancel it.'
|
|
45
|
+
: issue.state !== 'open' ? 'Only an open repository issue can start new work.'
|
|
46
|
+
: readiness.state === 'blocked' || readiness.state === 'conflicting' ? 'Resolve the readiness labels on the repository before starting work.' : null;
|
|
47
|
+
return { ...issue, identity, readiness, ...association, start_block_reason };
|
|
48
|
+
}
|
|
49
|
+
export function backlogHistory(jobs, issues) {
|
|
50
|
+
const loaded = new Set(issues.map(issue => issue.identity.key)), grouped = new Map();
|
|
51
|
+
for (const job of jobs) {
|
|
52
|
+
const identity = canonicalIssue(job.task?.source_url);
|
|
53
|
+
if (identity && loaded.has(identity.key)) continue;
|
|
54
|
+
const key = identity?.key || `local:${job.id}`;
|
|
55
|
+
if (!grouped.has(key)) grouped.set(key, { identity, key, title:job.task?.title || job.prompt?.split('\n')[0] || job.id, url:identity?.url || null,
|
|
56
|
+
source_status:identity ? 'not_loaded' : 'local', ...(identity ? executionAssociation(jobs, identity) : { executions:[{id:job.id,state:job.state,workflow:job.workflow?.name || null,phase:job.workflow?.steps?.[job.workflow.current_step] || null}] }) });
|
|
57
|
+
}
|
|
58
|
+
return [...grouped.values()];
|
|
59
|
+
}
|
package/factory/lib.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readinessMapping } from './issue-lifecycle.mjs';
|
|
1
2
|
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
2
3
|
import { resolve, dirname, join, isAbsolute } from 'node:path';
|
|
3
4
|
import { spawnSync, spawn } from 'node:child_process';
|
|
@@ -76,6 +77,7 @@ export function configAt(state) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
if (typeof config.check !== 'string' || !config.scope || !['project','service','environment','owner'].every(k=>typeof config.scope[k]==='string'&&config.scope[k].trim())) throw new Error('Missing check or installation scope');
|
|
80
|
+
readinessMapping(config.issueReadinessLabels);
|
|
79
81
|
validateWebVerification(config.webVerification);
|
|
80
82
|
return config;
|
|
81
83
|
}
|
|
@@ -37,8 +37,8 @@ export function githubIssueProvider(repo, { read = githubRead, write = githubWri
|
|
|
37
37
|
return {
|
|
38
38
|
id: 'github', label: 'GitHub', repository, supported: true,
|
|
39
39
|
capabilities: { issues: true, templates: true, create: true },
|
|
40
|
-
list: page => listIssues(repo, page, read),
|
|
41
|
-
preview: url => readIssue(repo, url, value => read(['issue', 'view', value, '--json', 'title,body,url,labels'])),
|
|
40
|
+
list: (page, state) => listIssues(repo, page, read, state),
|
|
41
|
+
preview: url => readIssue(repo, url, value => read(['issue', 'view', value, '--json', 'title,body,url,labels,state'])),
|
|
42
42
|
templates: () => readTemplates(repo, read),
|
|
43
43
|
draft: input => draftFromTemplate(repo, input, read),
|
|
44
44
|
async context() {
|