software-defence-factory 0.5.1 → 0.6.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 +35 -18
- package/docs/concepts.md +13 -8
- package/docs/integrations.md +67 -0
- package/docs/interfaces.md +9 -6
- package/docs/quickstart.md +1 -2
- package/docs/recovery.md +7 -0
- package/docs/setup.md +2 -2
- package/docs/workflows.md +42 -26
- package/factory/definition.mjs +1 -1
- package/factory/issue-provider.mjs +24 -0
- package/factory/issue-submissions.mjs +65 -0
- package/factory/providers/github.mjs +77 -0
- package/factory/server.mjs +17 -8
- package/factory/terminology.json +3 -2
- package/factory/ui/assets/index-CdazRQSz.js +13 -0
- package/factory/ui/assets/{index-qHVJlLfq.css → index-Whh9_Cb0.css} +1 -1
- package/factory/ui/index.html +2 -2
- package/kit/repository.md +14 -7
- package/operator-skills/factory-foundation/SKILL.md +11 -5
- package/package.json +3 -2
- package/factory/ui/assets/index-Ci1BkkmE.js +0 -13
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**. New issue offers the repository’s issue templates, a blank local form or a selectable GitHub issue.
|
|
40
|
+
The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**. New issue offers the repository’s issue templates, a blank local form or a selectable GitHub issue. 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
|
|
|
@@ -7,7 +7,6 @@ import { createServer } from 'node:net';
|
|
|
7
7
|
import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers } from '../factory/lib.mjs';
|
|
8
8
|
import { assertInstalledJobImage, installCustomJobImage, installStandardJobImage, inspectImageInstallation } from '../factory/image-install.mjs';
|
|
9
9
|
import { listIssues, readIssue } from '../factory/issue-intake.mjs';
|
|
10
|
-
import { readTemplates, draftFromTemplate } from '../factory/issue-templates.mjs';
|
|
11
10
|
import { recommendWork } from '../factory/intake.mjs';
|
|
12
11
|
import { factoryDefinition, foundationSkill } from '../factory/definition.mjs';
|
|
13
12
|
import { harnessOf } from '../factory/lib.mjs';
|
|
@@ -153,7 +152,7 @@ try {
|
|
|
153
152
|
}
|
|
154
153
|
else if(['infrastructure','automations','inbox'].includes(command)) {
|
|
155
154
|
const snapshot=await api(state,'/api/v1/status');
|
|
156
|
-
console.log(JSON.stringify(command==='inbox'?snapshot.jobs:snapshot[command],null,2));
|
|
155
|
+
console.log(JSON.stringify(command==='inbox'?snapshot.jobs:command==='automations'?snapshot.automation_control:snapshot[command],null,2));
|
|
157
156
|
}
|
|
158
157
|
else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
|
|
159
158
|
else if(command==='doctor') {
|
|
@@ -161,32 +160,46 @@ try {
|
|
|
161
160
|
console.log(JSON.stringify({node:process.version,docker:dockerVersion,engineInstalled:imageStatus.installed,image:imageStatus.image,repo:config.repo,harness:harnessOf(config),agent:harnessOf(config),checksConfigured:!!config.check?.trim(),inference:'Not called or verified',qualification:{model:'not assessed',toolchain:'not assessed'},dashboard:`http://127.0.0.1:${config.port}`},null,2));
|
|
162
161
|
if(!imageStatus.installed)process.exitCode=1;
|
|
163
162
|
} else if(command==='issue') {
|
|
164
|
-
const action=positional[0];
|
|
163
|
+
const action=positional[0], sourceURL=flags.url || flags.github;
|
|
165
164
|
if(action==='list') {
|
|
166
|
-
if(flags.source && !['factory','github'].includes(flags.source))throw new Error('Choose --source factory or
|
|
167
|
-
console.log(JSON.stringify(flags.source
|
|
168
|
-
} else if(action==='templates') console.log(JSON.stringify(await
|
|
165
|
+
if(flags.source && !['factory','remote','github'].includes(flags.source))throw new Error('Choose --source factory or remote');
|
|
166
|
+
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));
|
|
167
|
+
} else if(action==='templates') console.log(JSON.stringify(await api(state,'/api/v1/issue-templates'),null,2));
|
|
168
|
+
else if(action==='connection') console.log(JSON.stringify(await api(state,'/api/v1/issue-connection'),null,2));
|
|
169
|
+
else if(action==='submissions') console.log(JSON.stringify(await api(state,'/api/v1/issue-submissions'),null,2));
|
|
170
|
+
else if(action==='recover') {
|
|
171
|
+
if(!/^[A-Za-z0-9_-]{16,100}$/.test(flags.key || ''))throw new Error('Use --key with the saved request ID');
|
|
172
|
+
console.log(JSON.stringify(await api(state,`/api/v1/issue-submissions/${flags.key}/recover`,{}),null,2));
|
|
173
|
+
}
|
|
169
174
|
else if(action==='preview') {
|
|
170
|
-
if(!
|
|
171
|
-
console.log(JSON.stringify(await
|
|
175
|
+
if(!sourceURL)throw new Error('Use --url ISSUE_URL');
|
|
176
|
+
console.log(JSON.stringify(await api(state,'/api/v1/issues/preview',{url:sourceURL}),null,2));
|
|
172
177
|
} else if(action==='recommend') {
|
|
173
|
-
if(Boolean(
|
|
174
|
-
console.log(JSON.stringify(
|
|
178
|
+
if(Boolean(sourceURL)===Boolean(flags.file))throw new Error('Choose --file brief.md or --url ISSUE_URL');
|
|
179
|
+
console.log(JSON.stringify(sourceURL ? (await api(state,'/api/v1/issues/preview',{url:sourceURL})).recommendation : recommendWork({spec:readFileSync(resolve(flags.file),'utf8')}),null,2));
|
|
175
180
|
} else if(action==='draft') {
|
|
176
181
|
if(!flags.file||!flags.template||!flags.sha)throw new Error('Use --template NAME --sha SHA --file answers.json with {title, answers}');
|
|
177
|
-
console.log(JSON.stringify(await
|
|
182
|
+
console.log(JSON.stringify(await api(state,'/api/v1/issue-templates/draft',{...json(resolve(flags.file)),template:flags.template,sha:flags.sha}),null,2));
|
|
178
183
|
} else if(action==='create') {
|
|
184
|
+
if(flags.workflow||sourceURL)throw new Error('issue create publishes a new repository issue. Use issue start for execution.');
|
|
185
|
+
if(!flags.key)throw new Error('Provide a stable --key for safe retry and recovery.');
|
|
186
|
+
if(Boolean(flags.draft)===Boolean(flags.file))throw new Error('Choose --draft draft.json or --file brief.md --title TITLE');
|
|
187
|
+
const draft=flags.draft ? json(resolve(flags.draft)) : {title:flags.title,spec:readFileSync(resolve(flags.file),'utf8'),labels:[]};
|
|
188
|
+
const connection=await api(state,'/api/v1/issue-connection');
|
|
189
|
+
if(!connection.supported)throw new Error('No issue provider is available. Use issue start for a local brief.');
|
|
190
|
+
console.log(JSON.stringify(await api(state,'/api/v1/issues',{title:flags.title || draft.title,spec:draft.spec,labels:draft.labels || [],request_id:flags.key,repository:connection.repository,actor:connection.actor}),null,2));
|
|
191
|
+
} else if(action==='start') {
|
|
179
192
|
if(!['software','defence'].includes(flags.workflow))throw new Error('Review the issue and choose --workflow software or defence');
|
|
180
|
-
if([flags.file,
|
|
193
|
+
if([flags.file,sourceURL,flags.draft].filter(Boolean).length!==1)throw new Error('Choose --file brief.md, --draft draft.json or --url ISSUE_URL');
|
|
181
194
|
let input;
|
|
182
|
-
if(
|
|
195
|
+
if(sourceURL) { const issue=await api(state,'/api/v1/issues/preview',{url:sourceURL});input={title:issue.title,spec:issue.spec,source_url:issue.url}; }
|
|
183
196
|
else if(flags.draft) { const draft=json(resolve(flags.draft));input={title:draft.title,spec:draft.spec}; }
|
|
184
197
|
else input={title:flags.title,spec:readFileSync(resolve(flags.file),'utf8')};
|
|
185
198
|
input.title=flags.title || input.title;
|
|
186
199
|
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)');
|
|
187
200
|
if(flags.workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
|
|
188
201
|
console.log(JSON.stringify(await api(state,'/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || ''}),null,2));
|
|
189
|
-
} else throw new Error('Use issue list|templates|preview|recommend|draft|create; see help');
|
|
202
|
+
} else throw new Error('Use issue list|connection|templates|preview|recommend|draft|create|start|submissions|recover; see help');
|
|
190
203
|
} else if(command==='issues') {
|
|
191
204
|
console.log(JSON.stringify(await listIssues(configAt(state).repo,Number(flags.page || 1)),null,2));
|
|
192
205
|
} else if(command==='recommend') {
|
|
@@ -248,13 +261,17 @@ try {
|
|
|
248
261
|
service resume Release a reconciled maintenance reservation
|
|
249
262
|
tunnel install|start|stop|status|logs|uninstall --host SSH_ALIAS --port PORT
|
|
250
263
|
Persistent loopback SSH tunnel (macOS/Linux)
|
|
251
|
-
issue list [--source
|
|
264
|
+
issue list [--source remote] [--page N] List local executions or open repository issues
|
|
252
265
|
issue templates Read this repository's issue forms and contact links
|
|
253
|
-
issue preview --
|
|
254
|
-
issue recommend --file brief.md | --
|
|
266
|
+
issue preview --url URL Preview one repository issue without starting work
|
|
267
|
+
issue recommend --file brief.md | --url URL
|
|
255
268
|
issue draft --template NAME --sha SHA --file answers.json
|
|
256
269
|
Validate {title,answers}; output a local draft JSON
|
|
257
|
-
issue create --draft draft.json | --
|
|
270
|
+
issue create --draft draft.json | --file brief.md --title TITLE --key REQUEST_ID
|
|
271
|
+
Create a repository issue; does not execute work
|
|
272
|
+
issue connection | submissions Inspect provider identity or durable creation receipts
|
|
273
|
+
issue recover --key REQUEST_ID Reconcile an uncertain creation without another write
|
|
274
|
+
issue start --draft draft.json | --url URL | --file brief.md --title TITLE
|
|
258
275
|
--workflow software|defence [--model MODEL]
|
|
259
276
|
Create a local issue and start work; no GitHub write
|
|
260
277
|
issues [--page N] Browse open project issues, with next_page for more
|
package/docs/concepts.md
CHANGED
|
@@ -12,14 +12,16 @@ The CLI `definition` command and dashboard Definition page read that same catalo
|
|
|
12
12
|
| Agent | Responsibility and instructions | Implement, Review, Investigate; one shared harness/model profile |
|
|
13
13
|
| Skill | Reusable instructions | Six job skills; separate operator Factory Foundation |
|
|
14
14
|
| Workflow | Ordered steps and gates | Software: Implement → Check → Review → Accept; Defence: Investigate |
|
|
15
|
-
|
|
|
16
|
-
|
|
|
15
|
+
| Issue | Bounded work request | Remote issue lives in its provider; a local brief needs no remote issue |
|
|
16
|
+
| Execution | Admitted work and its attempts | Stored in the private SQLite queue with source link and evidence |
|
|
17
|
+
| Provider | Repository issue integration | GitHub adapter first; unknown remotes retain local execution |
|
|
18
|
+
| Automation | External schedule and agent context | Owned by the selected harness; calls Factory CLI/API, no Factory cron |
|
|
17
19
|
| Definition | Effective roles, workflows, skills and settings | Installed method plus private factory.json; read-only catalog |
|
|
18
20
|
|
|
19
|
-
Inbox contains
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Inbox contains execution history, not a copied remote issue backlog. New issue
|
|
22
|
+
can create a repository issue without execution; Start work admits execution
|
|
23
|
+
separately. The provider owns issue content/state; SQLite owns queue/attempts and
|
|
24
|
+
creation receipts for recovery. An unfinished local form is not a saved backlog.
|
|
23
25
|
An agent role is neither a machine nor a skill. Check is deterministic, and
|
|
24
26
|
Accept is an operator gate. Triage/specification precede admission; evaluation
|
|
25
27
|
is separately scoped work, not an automatic hidden agent phase.
|
|
@@ -51,7 +53,10 @@ Execution IDs (`build`, `verify`, `handoff`, `job_*`, `run_*`) are stable wire a
|
|
|
51
53
|
evidence identifiers. Human labels explain them without rewriting stored jobs.
|
|
52
54
|
Compatibility is handled at these boundaries; there is one active implementation.
|
|
53
55
|
|
|
54
|
-
CLI `issue` groups list, templates, preview, draft, recommend
|
|
56
|
+
CLI `issue` groups list, connection, templates, preview, draft, recommend, create,
|
|
57
|
+
start, submissions and recover. Since 0.6, `issue create` only publishes; migrate
|
|
58
|
+
0.5.1 execution callers to `issue start`. The
|
|
55
59
|
legacy `run`, `issues` (GitHub list) and `recommend` commands remain compatible.
|
|
56
60
|
Task/job field names and `/api/v1/jobs` are stable wire/storage identifiers for
|
|
57
|
-
these same local issues;
|
|
61
|
+
these same local issues; the extra SQLite receipt table is external-write bookkeeping, not an issue mirror
|
|
62
|
+
or another scheduler.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Repository and issue providers
|
|
2
|
+
|
|
3
|
+
Git source, issue tracking, execution and scheduling have different owners.
|
|
4
|
+
A Git remote does not guarantee an issue API, GitHub-compatible forms or a
|
|
5
|
+
particular CI system. Factory clones local Git source independently of the
|
|
6
|
+
optional issue integration.
|
|
7
|
+
|
|
8
|
+
| State | Owner |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| Source, branches, remote issues and their metadata | Selected repository/issue service |
|
|
11
|
+
| Queue, attempts, checks, approvals and evidence | Factory controller's private SQLite/files |
|
|
12
|
+
| External creation intent, correlation and receipt | SQLite; recovery bookkeeping, not an issue-backlog mirror |
|
|
13
|
+
| Schedules and scheduled-agent context | Selected harness, where it supports automations |
|
|
14
|
+
| Forms and actions | Dashboard and CLI over the same controller API |
|
|
15
|
+
|
|
16
|
+
## Current selection
|
|
17
|
+
|
|
18
|
+
`factory/issue-provider.mjs` selects an adapter from the configured Git origin.
|
|
19
|
+
Only public github.com origins select the implemented GitHub adapter. Its own
|
|
20
|
+
module owns GitHub API paths, host authentication, issue formats and recovery.
|
|
21
|
+
Unknown/self-hosted remotes, GitHub Enterprise, Forgejo/Gitea, GitLab and Cursor
|
|
22
|
+
Origin are **not implemented adapters**. They expose no remote-issue capability;
|
|
23
|
+
local brief execution and the method remain usable. Factory never sends GitHub
|
|
24
|
+
credentials to a guessed host or assumes a mirrored repo's original host.
|
|
25
|
+
|
|
26
|
+
The adapter contract supplies provider identity and explicit capabilities,
|
|
27
|
+
connection/acting identity, paged list, preview, templates/draft, publish and
|
|
28
|
+
read-only recovery. The generic submission store binds a request key to provider,
|
|
29
|
+
repository, identity and content before publication. Another provider can supply
|
|
30
|
+
these operations without changing the queue, controller routes or creation form.
|
|
31
|
+
Unsupported capabilities must fail visibly, not invent a compatible endpoint.
|
|
32
|
+
A non-GitHub test adapter exercises this boundary; it is not a shipped integration.
|
|
33
|
+
|
|
34
|
+
A future adapter must qualify its actual authentication, template/label model,
|
|
35
|
+
error behavior and ambiguous-write recovery. Add it at this boundary, with
|
|
36
|
+
matching CLI/API/UI support. Do not build a plugin loader, generalized OAuth
|
|
37
|
+
service or second database merely to reserve future extension points. Selecting
|
|
38
|
+
an issue tracker independently of the Git host remains a deliberate future
|
|
39
|
+
configuration extension, not an inferred mapping.
|
|
40
|
+
|
|
41
|
+
## GitHub adapter
|
|
42
|
+
|
|
43
|
+
The controller uses its existing `gh` identity. `issue connection` and the creation
|
|
44
|
+
form show the destination and acting user. GitHub enforces Issues write access;
|
|
45
|
+
label application needs suitable repository access. Browser GitHub login is not
|
|
46
|
+
required. Missing/revoked access produces a visible failure without execution.
|
|
47
|
+
No access token, browser cookie or host credential is passed to a job.
|
|
48
|
+
|
|
49
|
+
Creation sends the selected title/body/labels plus an opaque hidden correlation
|
|
50
|
+
marker. A lost response is recovered by bounded authenticated reads of issues
|
|
51
|
+
created by the original actor; no automatic second POST occurs. If the marker
|
|
52
|
+
was removed, the provider is unavailable or the bounded search cannot find it,
|
|
53
|
+
manual inspection is required. A receipt never claims current issue status;
|
|
54
|
+
read the provider again for current metadata. The published issue is the backlog.
|
|
55
|
+
|
|
56
|
+
Creation is separate from `issue start`. A remote issue never grants execution,
|
|
57
|
+
merge, deployment or production access. External harness automations use the same
|
|
58
|
+
explicit admission API and remain responsible for their own schedule/deduplication.
|
|
59
|
+
|
|
60
|
+
The managed updater’s OS timer maintains installed software; it is not a work-admission scheduler.
|
|
61
|
+
|
|
62
|
+
The dashboard derives its request key from the reviewed destination, identity,
|
|
63
|
+
title, body and labels. Reopening or reloading the same submission therefore
|
|
64
|
+
reuses its receipt, including a successful result whose browser response was
|
|
65
|
+
lost. No unsent draft is persisted by this mechanism. Identical browser content
|
|
66
|
+
returns the existing issue in that controller state; an intentional separate
|
|
67
|
+
copy requires a distinct CLI request key.
|
package/docs/interfaces.md
CHANGED
|
@@ -10,16 +10,17 @@ shell endpoint or a second scheduler.
|
|
|
10
10
|
| Capability | CLI | Shared API | Dashboard | Remaining work |
|
|
11
11
|
| --- | --- | --- | --- | --- |
|
|
12
12
|
| Project/queue/attempt state | `status`, `inbox` JSON | `GET /api/v1/status` | Project, tasks, details/history | Stable versioned agent result/error contract |
|
|
13
|
-
|
|
|
14
|
-
| Browse/import
|
|
13
|
+
| Start local work | `issue start --file --title`, `--draft` or `--url`, 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/import repository issues | `issue list --source remote [--page N]`, `issue preview --url URL` via controller provider | Authenticated `GET /api/v1/issues`, `POST /api/v1/issues/preview` using shared readers | Paged open-issue list, search loaded results, preview and explicit start for either type | Issue → execution links retained; no implicit polling |
|
|
15
|
+
| 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 |
|
|
15
16
|
| 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 |
|
|
16
|
-
| Suggest task type | `issue recommend --file` or `--
|
|
17
|
+
| 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 |
|
|
17
18
|
| Cancel/retry/approve | Commands | Job action endpoints with current run ID | Task controls | JSON action results and consistent needs-attention outcomes |
|
|
18
19
|
| Request changes | `revise --file` | `request_changes` action | Feedback form | JSON action result; retain shared stale-action guards |
|
|
19
20
|
| Remove a stopped task | No command | `DELETE /api/v1/jobs/:id` | Remove action | Add CLI; keep existing recoverability/history semantics |
|
|
20
21
|
| Evidence list/read/download | No command | Authenticated artifact routes | Files/preview/download | Add CLI with matching access and size/path rules |
|
|
21
22
|
| Roles, workflows and packaged skills | `definition`, `agents`, `skills` JSON (also while stopped) | `GET /api/v1/definitions` | Agents, Skills and Definition | Shared read-only catalog; future editing must preserve common policy/gates |
|
|
22
|
-
| Project repository links | Validated links in `status` | `project_links` from configured Git origin | View repo / optional GitHub issue link |
|
|
23
|
+
| Project repository links | Validated links in `status` | `project_links` from configured Git origin | View repo / optional GitHub issue link | Provider creates/issues reads are separate from Git source links |
|
|
23
24
|
| Recorded token usage | Per-attempt `usage` and `token_usage` in `status` | Same status records | Analytics, task rows, metadata/history | No billing estimate; partial/unknown coverage stays explicit |
|
|
24
25
|
| Analytics/filtering | Raw status available | Source queue records | Derived views | Expose equivalent queries/summaries without inventing usage data |
|
|
25
26
|
| Scoped incident admission | `incident --file` validates/deduplicates private evidence | No equivalent typed intake endpoint | Generic Defence form is not equivalent admission | Common typed intake, gaps and deduplication before execution |
|
|
@@ -46,8 +47,10 @@ authority. Headless use and GUI use must ultimately reach the same outcomes;
|
|
|
46
47
|
intermediate releases must explicitly retain their unimplemented rows here.
|
|
47
48
|
|
|
48
49
|
Infrastructure exposes detected host capacity and the local worker through
|
|
49
|
-
`infrastructure` and the same status API. `automations` returns the
|
|
50
|
-
|
|
50
|
+
`infrastructure` and the same status API. `automations` returns the harness
|
|
51
|
+
ownership contract; the UI explains that external schedules are not discovered.
|
|
52
|
+
Factory has no cron module. The v1 `automations: []` field remains a compatibility
|
|
53
|
+
view; `automation_control` owns the current semantics.
|
|
51
54
|
`foundation` prints the packaged operator skill without configuring anything;
|
|
52
55
|
the Skills page reads that same file. Full safe setup controls remain #37.
|
|
53
56
|
The roadmap is split into Defence #50, quality measurement #51, GitHub intake
|
package/docs/quickstart.md
CHANGED
|
@@ -98,8 +98,7 @@ Defence form is not that typed intake path.
|
|
|
98
98
|
|
|
99
99
|
The header names the configured project. View repo opens a validated GitHub
|
|
100
100
|
origin. New issue opens Factory’s local chooser: repository templates, a blank
|
|
101
|
-
form or existing GitHub issues.
|
|
102
|
-
start queues local work. See [intake and CLI examples](workflows.md). The task detail provides previous/next within the filtered list, copy
|
|
101
|
+
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
|
|
103
102
|
link and close (Escape). Closing preserves the list's filters and position.
|
|
104
103
|
|
|
105
104
|
If the interface looks unexpectedly small, check the browser zoom. The design
|
package/docs/recovery.md
CHANGED
|
@@ -99,3 +99,10 @@ can run. If an operation is interrupted, preserve the lock and inspect its PID
|
|
|
99
99
|
and action. Remove it only after confirming that process and its image build or
|
|
100
100
|
startup have stopped, then run `doctor` and reconcile image metadata before
|
|
101
101
|
starting again. The CLI does not automatically clear an unknown lock.
|
|
102
|
+
|
|
103
|
+
## Unconfirmed repository issue creation
|
|
104
|
+
|
|
105
|
+
Use `issue submissions` and `issue recover --key REQUEST_ID` against the same
|
|
106
|
+
controller state. Recovery reads the original provider using the original
|
|
107
|
+
identity; it does not publish again. Do not use a new request key to retry an
|
|
108
|
+
uncertain write. See [provider ownership and recovery limits](integrations.md).
|
package/docs/setup.md
CHANGED
|
@@ -103,7 +103,7 @@ qualification record; stop this controller unless it is the selected dashboard.
|
|
|
103
103
|
## 4. Prepare each application and inference profile
|
|
104
104
|
|
|
105
105
|
Preserve existing commits, branches, uncommitted work and licenses before moving
|
|
106
|
-
anything. Verify the canonical
|
|
106
|
+
anything. Verify the canonical Git origin and main branch's tracking target;
|
|
107
107
|
a fork may still track its upstream product. Move application sources into the
|
|
108
108
|
chosen workspace, not into the npm package or private runtime state.
|
|
109
109
|
|
|
@@ -115,7 +115,7 @@ Before admitting development work, establish:
|
|
|
115
115
|
- Reproducible toolchain/dependency pins and an actual build/check command.
|
|
116
116
|
- A compatible job image for native libraries, browser/mobile tools or custom
|
|
117
117
|
model adapters; the standard image does not supply every application stack.
|
|
118
|
-
- Meaningful checks in
|
|
118
|
+
- Meaningful checks in the selected CI on the selected revision, plus the intended
|
|
119
119
|
review/branch policy. Record plan/access limits if enforcement is unavailable.
|
|
120
120
|
- Applicable application instructions, design/scope, resource limits, secret
|
|
121
121
|
references and a clear delivery destination. Keep unfinished WIP separate
|
package/docs/workflows.md
CHANGED
|
@@ -31,7 +31,7 @@ The list excludes pull requests, loads 50 GitHub records per page and offers
|
|
|
31
31
|
**Load more**; search filters the loaded issues by title, number or label. GitHub
|
|
32
32
|
reads use the controller's existing access, with retry on failure.
|
|
33
33
|
|
|
34
|
-
Review the instructions and suggested work type before
|
|
34
|
+
Review the instructions and suggested work type before explicitly starting work. The shared,
|
|
35
35
|
deterministic suggestion prioritizes `track:software` and `track:security` (also
|
|
36
36
|
`track:defence`/`track:defense`) labels. Without a track label, explicit incident
|
|
37
37
|
investigation wording suggests Defence; otherwise Software is the default.
|
|
@@ -48,40 +48,56 @@ visible with a GitHub link instead of silently dropping fields. Contact links
|
|
|
48
48
|
preserve the project's private security reporting route. Template Markdown is
|
|
49
49
|
shown as literal text, never executed or rendered as raw HTML.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
On a supported repository, **Create issue on GitHub** writes the title, description
|
|
52
|
+
and template labels to that repository using the displayed host identity. It
|
|
53
|
+
returns the real issue number/link and **does not start execution**. Select
|
|
54
|
+
**Start work** separately, or choose the issue later from the repository list.
|
|
55
|
+
Choose **Local execution only** to submit a brief without publishing it. A local
|
|
56
|
+
brief is an execution request; an unfinished form is not a persistent backlog.
|
|
57
|
+
Use the private security contact route for sensitive reports, never a public issue.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
The controller records a durable creation receipt before calling the provider.
|
|
60
|
+
After a timeout, **Check submission** or `issue recover` looks for the original
|
|
61
|
+
result; it never blindly repeats a write. Reuse the same request key on CLI retries.
|
|
62
|
+
Changed content/identity with that key is rejected. An unresolved result stays
|
|
63
|
+
unconfirmed rather than risking a duplicate. Confirmed missing labels are shown;
|
|
64
|
+
GitHub projects, assignees and arbitrary issue form extensions are not applied.
|
|
65
|
+
|
|
66
|
+
CLI equivalents (the selected controller must be running):
|
|
60
67
|
|
|
61
68
|
```sh
|
|
62
|
-
software-defence-factory issue
|
|
63
|
-
software-defence-factory issue list --source
|
|
64
|
-
software-defence-factory issue preview --github URL --state PATH
|
|
69
|
+
software-defence-factory issue connection --state PATH
|
|
70
|
+
software-defence-factory issue list --source remote --state PATH --page 1
|
|
65
71
|
software-defence-factory issue templates --state PATH
|
|
66
72
|
software-defence-factory issue draft --state PATH --template bug-report.yml --sha TEMPLATE_SHA --file answers.json > draft.json
|
|
67
|
-
software-defence-factory issue create --state PATH --draft draft.json --
|
|
68
|
-
software-defence-factory issue
|
|
69
|
-
software-defence-factory issue
|
|
70
|
-
|
|
73
|
+
software-defence-factory issue create --state PATH --draft draft.json --key release-board-fix-01
|
|
74
|
+
software-defence-factory issue submissions --state PATH
|
|
75
|
+
software-defence-factory issue recover --state PATH --key release-board-fix-01
|
|
76
|
+
# Explicit execution, independent of creation:
|
|
77
|
+
software-defence-factory issue start --state PATH --url URL --workflow software
|
|
78
|
+
software-defence-factory issue start --state PATH --file brief.md --title "Investigate supplied evidence" --workflow defence
|
|
71
79
|
```
|
|
72
80
|
|
|
73
81
|
`answers.json` contains `{"title":"Fix the board","answers":{"problem":"..."}}`;
|
|
74
82
|
keys match `fields[].id` in `issue templates`. Multi-select/checkbox answers are
|
|
75
|
-
arrays of exact option labels.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
View repo
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
arrays of exact option labels. `issue create` now publishes only; migrate 0.5.1
|
|
84
|
+
execution scripts to `issue start`. Legacy `run` remains compatible. Typed private
|
|
85
|
+
incident admission remains `incident --file`, distinct from a generic Defence brief.
|
|
86
|
+
`--source github` remains an alias for repository listing; `--github URL` remains a compatibility alias for `--url URL`.
|
|
87
|
+
|
|
88
|
+
Provider selection and unknown-host behavior are documented in [integrations](integrations.md).
|
|
89
|
+
View repo uses the browser's own login. Factory's provider uses the controller
|
|
90
|
+
host identity; neither shares credentials with the browser or job containers.
|
|
91
|
+
|
|
92
|
+
## Scheduled work
|
|
93
|
+
|
|
94
|
+
Configure schedules in the selected harness, where supported (for example Codex
|
|
95
|
+
Automations). The scheduled agent calls Factory CLI/API with explicitly selected
|
|
96
|
+
scope. Factory owns execution, checks and acceptance, not the external schedule.
|
|
97
|
+
There is no Factory cron module, issue watcher or silently enabled automation.
|
|
98
|
+
The Automations view identifies this owner; it does not claim to discover external
|
|
99
|
+
schedules. Before enabling one, test its host availability, access, duplicate
|
|
100
|
+
handling, resource limits and stop behavior. No schedule is created by onboarding.
|
|
85
101
|
|
|
86
102
|
## Change the definition
|
|
87
103
|
|
package/factory/definition.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export function factoryDefinition(config) {
|
|
|
34
34
|
phase, title: info.title, responsibility: info.description, harness, model: config.model || null, skills: info.skills,
|
|
35
35
|
})),
|
|
36
36
|
operator_skills: [foundationSkill()],
|
|
37
|
-
automations: {
|
|
37
|
+
automations: { owner: 'harness', harness, managed_by_factory: false, discovery: 'unavailable', items: null },
|
|
38
38
|
commands: Object.entries(phaseInfo).map(([name, info]) => ({ name, ...info, prompt: info.description,
|
|
39
39
|
executor: info.owner === 'agent' ? harnessOf(config) : 'factory', timeout: `${config.timeoutSeconds}s` })),
|
|
40
40
|
skills,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { githubIssueProvider } from './providers/github.mjs';
|
|
3
|
+
import { readProjectLinks } from './project-links.mjs';
|
|
4
|
+
import { QueueError } from './queue.mjs';
|
|
5
|
+
|
|
6
|
+
// Selection is local and capability-based. Unknown hosts never receive a GitHub
|
|
7
|
+
// token or a guessed API request. Git checkout/execution does not need an adapter.
|
|
8
|
+
export function issueProvider(repo) {
|
|
9
|
+
if (readProjectLinks(repo)) return githubIssueProvider(repo);
|
|
10
|
+
let host = null;
|
|
11
|
+
try {
|
|
12
|
+
const remote = execFileSync('git', ['-c', 'core.fsmonitor=false', '-C', repo, 'config', '--local', '--get', 'remote.origin.url'], { encoding:'utf8', timeout:2000, maxBuffer:4096, stdio:['ignore','pipe','ignore'], env:{...process.env,GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null'} }).trim();
|
|
13
|
+
if (/^(https?|ssh):\/\//.test(remote)) host = new URL(remote).hostname;
|
|
14
|
+
else host = remote.match(/^(?:[^@\s]+@)?([a-zA-Z0-9.-]+):[^/]/)?.[1] || null;
|
|
15
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(host || '')) host = null;
|
|
16
|
+
} catch { /* No recognizable network origin; local execution remains available. */ }
|
|
17
|
+
const unavailable = async () => { throw new QueueError('This repository has no supported issue provider. Use a local brief; no remote issue will be created.', 400); };
|
|
18
|
+
return { id:'unsupported', label:'Repository host', repository:null, host, supported:false,
|
|
19
|
+
capabilities:{issues:false,templates:false,create:false},
|
|
20
|
+
context:unavailable, list:unavailable, preview:unavailable, templates:unavailable, draft:unavailable, publish:unavailable, recover:unavailable };
|
|
21
|
+
}
|
|
22
|
+
export function providerInfo(provider) {
|
|
23
|
+
return {id:provider.id,label:provider.label,repository:provider.repository,host:provider.host || null,supported:provider.supported,capabilities:provider.capabilities};
|
|
24
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { QueueError } from './queue.mjs';
|
|
3
|
+
const keyPattern = /^[A-Za-z0-9_-]{16,100}$/;
|
|
4
|
+
const fingerprint = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
5
|
+
|
|
6
|
+
// Receipts share the controller database and maintenance lock. They are not jobs
|
|
7
|
+
// and cannot schedule execution. A pending write is never blindly repeated.
|
|
8
|
+
export class IssueSubmissions {
|
|
9
|
+
constructor(queue, provider) {
|
|
10
|
+
this.queue = queue; this.provider = provider;
|
|
11
|
+
queue.db.exec('CREATE TABLE IF NOT EXISTS issue_submissions(id TEXT PRIMARY KEY, data TEXT NOT NULL)');
|
|
12
|
+
}
|
|
13
|
+
get(key) {
|
|
14
|
+
if (!keyPattern.test(key || '')) throw new QueueError('Provide a stable request key of 16–100 letters, digits, hyphens or underscores.', 400);
|
|
15
|
+
const row = this.queue.db.prepare('SELECT data FROM issue_submissions WHERE id=?').get(key);
|
|
16
|
+
return row && JSON.parse(row.data);
|
|
17
|
+
}
|
|
18
|
+
save(record) { this.queue.db.prepare('INSERT INTO issue_submissions VALUES (?,?) ON CONFLICT(id) DO UPDATE SET data=excluded.data').run(record.request_id, JSON.stringify(record)); return this.present(record); }
|
|
19
|
+
present(record) {
|
|
20
|
+
return { request_id: record.request_id, state: record.state, provider: record.provider, repository: record.repository, actor: record.actor,
|
|
21
|
+
title: record.payload.title, created_at: record.created_at, issue: record.issue || null, error: record.error || null };
|
|
22
|
+
}
|
|
23
|
+
list() { return this.queue.db.prepare('SELECT data FROM issue_submissions ORDER BY rowid DESC LIMIT 50').all().map(row => this.present(JSON.parse(row.data))); }
|
|
24
|
+
create(input) { return this.queue.exclusive(`issue-submission:${input.request_id}`, () => this.publish(input)); }
|
|
25
|
+
async publish(input) {
|
|
26
|
+
const existing = this.get(input.request_id);
|
|
27
|
+
if (typeof input.title !== 'string' || !input.title.trim() || input.title.length > 160 || typeof input.spec !== 'string' || !input.spec.trim() || Buffer.byteLength(input.spec) > 60000) throw new QueueError('Provide a title under 161 characters and a description under 60 KB.', 400);
|
|
28
|
+
if (!Array.isArray(input.labels) || input.labels.length > 50 || input.labels.some(label => typeof label !== 'string' || !label.trim() || label.length > 100)) throw new QueueError('Provide up to 50 valid issue labels.', 400);
|
|
29
|
+
const payload = { title: input.title.trim(), body: input.spec.trim(), labels: [...new Set(input.labels)].sort() };
|
|
30
|
+
const context = await this.provider.context();
|
|
31
|
+
if (input.repository !== context.repository || input.actor !== context.actor) throw new QueueError('Repository destination or identity changed. Review the connection before creating.', 409);
|
|
32
|
+
const hash = fingerprint({ provider: this.provider.id, repository: context.repository, actor_id: context.actor_id, payload });
|
|
33
|
+
if (existing) {
|
|
34
|
+
if (existing.hash !== hash) throw new QueueError('This request key already belongs to different content or identity. Inspect its receipt before creating another issue.', 409);
|
|
35
|
+
if (existing.state === 'created') return this.present(existing);
|
|
36
|
+
if (existing.state !== 'rejected') return this.reconcile(existing);
|
|
37
|
+
}
|
|
38
|
+
if (!context.available) throw new QueueError('This repository is archived or has issues disabled.', 400);
|
|
39
|
+
if (payload.labels.length && !context.labels_supported) throw new QueueError('This identity cannot apply the selected labels. Use a repository identity with label access.', 403);
|
|
40
|
+
const record = existing || { request_id: input.request_id, provider: this.provider.id, hash, repository: context.repository, actor: context.actor, actor_id: context.actor_id,
|
|
41
|
+
payload, correlation_id: randomUUID(), created_at: new Date().toISOString() };
|
|
42
|
+
record.state = 'pending'; record.error = null; this.save(record);
|
|
43
|
+
try {
|
|
44
|
+
record.issue = await this.provider.publish(record);
|
|
45
|
+
record.state = 'created'; return this.save(record);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
record.state = [400,401,403,404,410,422,429].includes(error.httpStatus) ? 'rejected' : 'uncertain';
|
|
48
|
+
record.error = record.state === 'rejected' ? 'Repository provider rejected creation. Check Issues write access and repository labels, then retry the same request.' : 'Creation may have succeeded. Check this saved submission; do not create another issue to retry.';
|
|
49
|
+
this.save(record);
|
|
50
|
+
throw new QueueError(`${record.error} Request: ${record.request_id}`, 409);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
recover(key) { return this.queue.exclusive(`issue-submission:${key}`, async () => {
|
|
54
|
+
const record = this.get(key); if (!record) throw new QueueError('Submission not found.', 404);
|
|
55
|
+
const context = await this.provider.context();
|
|
56
|
+
if (record.provider !== this.provider.id || record.repository !== context.repository || record.actor_id !== context.actor_id) throw new QueueError('Restore the original repository and provider identity before recovering this submission.', 409);
|
|
57
|
+
if (record.state === 'created' || record.state === 'rejected') return this.present(record);
|
|
58
|
+
return this.reconcile(record);
|
|
59
|
+
}); }
|
|
60
|
+
async reconcile(record) {
|
|
61
|
+
const issue = await this.provider.recover(record);
|
|
62
|
+
record.issue = issue; record.state = 'created'; record.error = null;
|
|
63
|
+
return this.save(record);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { githubRead, listIssues, readIssue } from '../issue-intake.mjs';
|
|
3
|
+
import { readTemplates, draftFromTemplate } from '../issue-templates.mjs';
|
|
4
|
+
import { readProjectLinks } from '../project-links.mjs';
|
|
5
|
+
import { QueueError } from '../queue.mjs';
|
|
6
|
+
const marker = record => `<!-- factory-issue:${record.correlation_id} -->`;
|
|
7
|
+
const apiArgs = path => ['api', '--hostname', 'github.com', path];
|
|
8
|
+
export function githubWrite(path, payload) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = execFile('gh', [...apiArgs(path), '--method', 'POST', '--input', '-'], {
|
|
11
|
+
timeout: 15000, maxBuffer: 1024 * 1024, encoding: 'utf8',
|
|
12
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_PAGER: 'cat' },
|
|
13
|
+
}, (cause, stdout, stderr) => {
|
|
14
|
+
if (cause) {
|
|
15
|
+
const error = new Error('GitHub did not confirm issue creation.');
|
|
16
|
+
error.httpStatus = Number(stderr?.match(/\(HTTP (\d+)\)/)?.[1]) || undefined;
|
|
17
|
+
reject(error);
|
|
18
|
+
} else {
|
|
19
|
+
try { resolve(JSON.parse(stdout)); } catch { reject(new Error('GitHub returned an unreadable creation result.')); }
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
child.stdin.on('error', () => {}); // Process completion reports a broken pipe without exposing request content.
|
|
23
|
+
child.stdin.end(JSON.stringify(payload));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function confirm(record, issue) {
|
|
28
|
+
if (issue?.pull_request || !Number.isSafeInteger(issue?.number) || issue.number < 1 || issue.html_url !== `${record.repository}/issues/${issue.number}` || issue.user?.id !== record.actor_id || !issue.body?.includes(marker(record))) throw new Error('Unexpected GitHub creation result.');
|
|
29
|
+
const actualLabels = (issue.labels || []).map(label => label.name);
|
|
30
|
+
const result = { number: issue.number, url: issue.html_url, title: issue.title, labels: actualLabels,
|
|
31
|
+
missing_labels: record.payload.labels.filter(label => !actualLabels.includes(label)) };
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function githubIssueProvider(repo, { read = githubRead, write = githubWrite } = {}) {
|
|
36
|
+
const repository = readProjectLinks(repo)?.repository;
|
|
37
|
+
return {
|
|
38
|
+
id: 'github', label: 'GitHub', repository, supported: true,
|
|
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'])),
|
|
42
|
+
templates: () => readTemplates(repo, read),
|
|
43
|
+
draft: input => draftFromTemplate(repo, input, read),
|
|
44
|
+
async context() {
|
|
45
|
+
const repository = readProjectLinks(repo)?.repository;
|
|
46
|
+
if (!repository) throw new QueueError('Configure a GitHub origin for this project.', 400);
|
|
47
|
+
const slug = repository.slice('https://github.com/'.length);
|
|
48
|
+
let user, project;
|
|
49
|
+
try { [user, project] = await Promise.all([read(apiArgs('user')), read(apiArgs(`repos/${slug}`))]); }
|
|
50
|
+
catch { throw new QueueError('GitHub access unavailable. Check gh authentication on the controller host.', 400); }
|
|
51
|
+
if (!Number.isSafeInteger(user?.id) || !/^[A-Za-z0-9-]+$/.test(user?.login || '') || project?.full_name?.toLowerCase() !== slug.toLowerCase()) throw new QueueError('GitHub returned an unexpected identity or repository.', 400);
|
|
52
|
+
return { repository, actor: user.login, actor_id: user.id, available: !project.archived && project.has_issues === true,
|
|
53
|
+
labels_supported: Boolean(project.permissions?.push || project.permissions?.triage || project.permissions?.maintain || project.permissions?.admin),
|
|
54
|
+
permission: 'GitHub checks Issues write permission when creating. No credentials are sent to the browser or jobs.' };
|
|
55
|
+
},
|
|
56
|
+
async publish(record) {
|
|
57
|
+
const slug = record.repository.slice('https://github.com/'.length);
|
|
58
|
+
const issue = await write(`repos/${slug}/issues`, { ...record.payload, body: `${record.payload.body}\n\n${marker(record)}` });
|
|
59
|
+
return confirm(record, issue);
|
|
60
|
+
},
|
|
61
|
+
async recover(record) {
|
|
62
|
+
const slug = record.repository.slice('https://github.com/'.length);
|
|
63
|
+
const since = new Date(Date.parse(record.created_at) - 60000).toISOString();
|
|
64
|
+
try {
|
|
65
|
+
for (let page = 1; page <= 5; page++) {
|
|
66
|
+
const result = await read(apiArgs(`repos/${slug}/issues?state=all&creator=${encodeURIComponent(record.actor)}&since=${encodeURIComponent(since)}&sort=created&direction=desc&per_page=100&page=${page}`));
|
|
67
|
+
if (!Array.isArray(result)) throw new Error('Unexpected issue listing');
|
|
68
|
+
const matches = result.filter(issue => !issue.pull_request && issue.user?.id === record.actor_id && issue.body?.includes(marker(record)));
|
|
69
|
+
if (matches.length > 1) throw new Error('Multiple matching issues');
|
|
70
|
+
if (matches.length === 1) return confirm(record, matches[0]);
|
|
71
|
+
if (result.length < 100) break;
|
|
72
|
+
}
|
|
73
|
+
} catch { throw new QueueError(`Could not reconcile submission ${record.request_id}. No write was retried. Check GitHub access and retry recovery.`, 409); }
|
|
74
|
+
throw new QueueError(`Submission ${record.request_id} is still unconfirmed. No write was retried. Inspect GitHub before creating another issue.`, 409);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|