software-defence-factory 0.5.0 → 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.
@@ -1,4 +1,4 @@
1
- name: Factory task
1
+ name: Factory issue
2
2
  description: Describe a business outcome and observable acceptance criteria.
3
3
  title: "[Factory] "
4
4
  labels: ["factory:triage"]
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 task opens a local brief; importing a GitHub issue is optional. 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). Automations are not implemented yet; work starts manually.
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
 
@@ -6,7 +6,8 @@ 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 } from '../factory/lib.mjs';
8
8
  import { assertInstalledJobImage, installCustomJobImage, installStandardJobImage, inspectImageInstallation } from '../factory/image-install.mjs';
9
- import { readIssue } from '../factory/issue-intake.mjs';
9
+ import { listIssues, readIssue } from '../factory/issue-intake.mjs';
10
+ import { recommendWork } from '../factory/intake.mjs';
10
11
  import { factoryDefinition, foundationSkill } from '../factory/definition.mjs';
11
12
  import { harnessOf } from '../factory/lib.mjs';
12
13
  import { admitIncident } from '../factory/incident.mjs';
@@ -151,21 +152,70 @@ try {
151
152
  }
152
153
  else if(['infrastructure','automations','inbox'].includes(command)) {
153
154
  const snapshot=await api(state,'/api/v1/status');
154
- 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));
155
156
  }
156
157
  else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
157
158
  else if(command==='doctor') {
158
159
  const config=configAt(state),dockerVersion=run('docker',['info','--format','{{.ServerVersion}}']),imageStatus=inspectImageInstallation(state,config);
159
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));
160
161
  if(!imageStatus.installed)process.exitCode=1;
162
+ } else if(command==='issue') {
163
+ const action=positional[0], sourceURL=flags.url || flags.github;
164
+ if(action==='list') {
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
+ }
174
+ else if(action==='preview') {
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));
177
+ } else if(action==='recommend') {
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));
180
+ } else if(action==='draft') {
181
+ if(!flags.file||!flags.template||!flags.sha)throw new Error('Use --template NAME --sha SHA --file answers.json with {title, answers}');
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));
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') {
192
+ if(!['software','defence'].includes(flags.workflow))throw new Error('Review the issue and choose --workflow software or defence');
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');
194
+ let input;
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}; }
196
+ else if(flags.draft) { const draft=json(resolve(flags.draft));input={title:draft.title,spec:draft.spec}; }
197
+ else input={title:flags.title,spec:readFileSync(resolve(flags.file),'utf8')};
198
+ input.title=flags.title || input.title;
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)');
200
+ if(flags.workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
201
+ console.log(JSON.stringify(await api(state,'/api/v1/jobs',{...input,workflow:flags.workflow,repository:'app',model:flags.model || ''}),null,2));
202
+ } else throw new Error('Use issue list|connection|templates|preview|recommend|draft|create|start|submissions|recover; see help');
203
+ } else if(command==='issues') {
204
+ console.log(JSON.stringify(await listIssues(configAt(state).repo,Number(flags.page || 1)),null,2));
205
+ } else if(command==='recommend') {
206
+ if(Boolean(flags.issue) === Boolean(flags.file))throw new Error('Choose --file task.md or --issue URL');
207
+ const recommendation=flags.issue ? (await readIssue(configAt(state).repo,flags.issue)).recommendation : recommendWork({spec:readFileSync(resolve(flags.file),'utf8')});
208
+ console.log(JSON.stringify(recommendation,null,2));
161
209
  } else if(command==='run') {
210
+ const workflow=flags.workflow || 'software';
211
+ if(!['software','defence'].includes(workflow))throw new Error('Choose --workflow software or defence');
162
212
  let spec;
163
213
  if(flags.issue) {
164
214
  spec=(await readIssue(configAt(state).repo,flags.issue)).spec;
165
215
  } else if(flags.file)spec=readFileSync(resolve(flags.file),'utf8');
166
216
  else throw new Error('Use --file task.md or --issue https://github.com/owner/repo/issues/123');
167
- if(!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
168
- console.log(JSON.stringify(await submit('software',spec)));
217
+ if(workflow==='software'&&!configAt(state).check?.trim())throw new Error('Configure an app check before submitting software work');
218
+ console.log(JSON.stringify(await submit(workflow,spec)));
169
219
  } else if(command==='incident') {
170
220
  if(!flags.file)throw new Error('Use --file incident.json; see factory/examples/incident.json');
171
221
  console.log(JSON.stringify(await admitIncident(state,json(resolve(flags.file)),submit)));
@@ -211,7 +261,22 @@ try {
211
261
  service resume Release a reconciled maintenance reservation
212
262
  tunnel install|start|stop|status|logs|uninstall --host SSH_ALIAS --port PORT
213
263
  Persistent loopback SSH tunnel (macOS/Linux)
214
- run --file task.md | --issue URL Submit one software vertical slice
264
+ issue list [--source remote] [--page N] List local executions or open repository issues
265
+ issue templates Read this repository's issue forms and contact links
266
+ issue preview --url URL Preview one repository issue without starting work
267
+ issue recommend --file brief.md | --url URL
268
+ issue draft --template NAME --sha SHA --file answers.json
269
+ Validate {title,answers}; output a local 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
275
+ --workflow software|defence [--model MODEL]
276
+ Create a local issue and start work; no GitHub write
277
+ issues [--page N] Browse open project issues, with next_page for more
278
+ recommend --file task.md | --issue URL Suggest a work type without starting work
279
+ run --file task.md | --issue URL Submit software (default), or --workflow defence
215
280
  incident --file incident.json Submit a private, read-only incident draft
216
281
  approve JOB_ID | cancel JOB_ID Review gate / stop this attempt
217
282
  retry JOB_ID Prove stop; retain old checkout and retry
package/docs/concepts.md CHANGED
@@ -12,12 +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
- | Task | Bounded admitted work | A stored job with retained run attempts; not a GitHub issue |
16
- | Automation | Trigger, filters and target | Planned; work starts manually |
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 admitted tasks, not an imported GitHub backlog. An issue is source
20
- material; importing it previews the scope and starting a task queues execution.
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.
21
25
  An agent role is neither a machine nor a skill. Check is deterministic, and
22
26
  Accept is an operator gate. Triage/specification precede admission; evaluation
23
27
  is separately scoped work, not an automatic hidden agent phase.
@@ -26,7 +30,7 @@ All job skills are available read-only. Role instructions identify relevant
26
30
  skills; per-role skill/access/harness profiles are not yet supported. Inference
27
31
  authentication, GitHub identity and SSH access are separate boundaries. Browser
28
32
  GitHub sign-in does not configure `gh` on the controller host, and host `gh` auth
29
- does not sign a browser in. Writing a local task needs neither a GitHub issue nor
33
+ does not sign a browser in. Writing a blank local issue needs neither a GitHub issue nor
30
34
  browser GitHub login. Import uses the controller's configured-repository `gh`
31
35
  access. See [interface support](interfaces.md).
32
36
 
@@ -48,3 +52,11 @@ private so existing installations can upgrade without credential migration.
48
52
  Execution IDs (`build`, `verify`, `handoff`, `job_*`, `run_*`) are stable wire and
49
53
  evidence identifiers. Human labels explain them without rewriting stored jobs.
50
54
  Compatibility is handled at these boundaries; there is one active implementation.
55
+
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
59
+ legacy `run`, `issues` (GitHub list) and `recommend` commands remain compatible.
60
+ Task/job field names and `/api/v1/jobs` are stable wire/storage identifiers for
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.
@@ -10,14 +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
- | Submit software text | `run --file` | `POST /api/v1/jobs` | New task modal | Equivalent title/source/model options and validation |
14
- | Import a GitHub issue | `run --issue` via operator `gh` | Authenticated `POST /api/v1/issues/preview` using the same reader | Import, review and explicitly start | Backlog/listing, stronger issue/job identity and optional qualified triggers remain; no implicit polling |
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 |
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 |
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 |
15
18
  | Cancel/retry/approve | Commands | Job action endpoints with current run ID | Task controls | JSON action results and consistent needs-attention outcomes |
16
19
  | Request changes | `revise --file` | `request_changes` action | Feedback form | JSON action result; retain shared stale-action guards |
17
20
  | Remove a stopped task | No command | `DELETE /api/v1/jobs/:id` | Remove action | Add CLI; keep existing recoverability/history semantics |
18
21
  | Evidence list/read/download | No command | Authenticated artifact routes | Files/preview/download | Add CLI with matching access and size/path rules |
19
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 |
20
- | Project repository links | Validated links in `status` | `project_links` from configured Git origin | View repo / optional GitHub issue link | Links only; no issue synchronization or creation API |
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 |
21
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 |
22
25
  | Analytics/filtering | Raw status available | Source queue records | Derived views | Expose equivalent queries/summaries without inventing usage data |
23
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 |
@@ -44,8 +47,10 @@ authority. Headless use and GUI use must ultimately reach the same outcomes;
44
47
  intermediate releases must explicitly retain their unimplemented rows here.
45
48
 
46
49
  Infrastructure exposes detected host capacity and the local worker through
47
- `infrastructure` and the same status API. `automations` returns the supported
48
- empty list; the UI explicitly states that automatic admission is unavailable.
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.
49
54
  `foundation` prints the packaged operator skill without configuring anything;
50
55
  the Skills page reads that same file. Full safe setup controls remain #37.
51
56
  The roadmap is split into Defence #50, quality measurement #51, GitHub intake
@@ -88,7 +88,7 @@ No Docker socket, operator credentials or unrelated project caches are mounted.
88
88
 
89
89
  ## Read the project dashboard
90
90
 
91
- Tasks contains both Software delivery and Defence investigation. Select a
91
+ Inbox contains both Software delivery and Defence investigation. Select a
92
92
  workflow to focus the list; workflow, requested-model, status-badge and text
93
93
  filters combine. Analytics offers the same workflow separation for recorded
94
94
  outcomes, duration and [token usage](usage.md). An issue link is a reference,
@@ -96,9 +96,9 @@ not an execution type. For validated, deduplicated private incident intake,
96
96
  use the [Defence integration](defence-integration.md) recipe; the generic
97
97
  Defence form is not that typed intake path.
98
98
 
99
- The header names the configured project. View repo and New issue appear only
100
- for a validated GitHub origin; they open GitHub and do not synchronize its
101
- backlog. The task detail provides previous/next within the filtered list, copy
99
+ The header names the configured project. View repo opens a validated GitHub
100
+ origin. New issue opens Factory’s local chooser: repository templates, a blank
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
102
102
  link and close (Escape). Closing preserves the list's filters and position.
103
103
 
104
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 GitHub origin and main branch's tracking target;
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 GitHub CI on the selected revision, plus the intended
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
@@ -24,16 +24,80 @@ kept outside those execution mounts.
24
24
 
25
25
  ## Start work
26
26
 
27
- Open **New task** in Inbox, write a bounded brief and start it. Alternatively,
28
- choose **From GitHub issue**, load an issue from the configured repository,
29
- review the imported scope and start it. Starting queues real work; creating an
30
- issue or setting a label does not. The CLI equivalent is `run --file task.md`
31
- or `run --issue URL`. Private validated incident intake uses `incident --file`.
32
-
33
- View repo opens GitHub in a new tab, where the browser's own login applies.
34
- Import uses the controller host's `gh` identity; it does not borrow browser
35
- credentials. Import previews neither enable automatic triggers nor grant an
36
- issue author more access.
27
+ Open **New issue** in Inbox, choose a repository template (or **Blank issue**),
28
+ complete the title and fields, then **Continue**. Or choose
29
+ **From GitHub issues** and select an open issue from the configured repository.
30
+ The list excludes pull requests, loads 50 GitHub records per page and offers
31
+ **Load more**; search filters the loaded issues by title, number or label. GitHub
32
+ reads use the controller's existing access, with retry on failure.
33
+
34
+ Review the instructions and suggested work type before explicitly starting work. The shared,
35
+ deterministic suggestion prioritizes `track:software` and `track:security` (also
36
+ `track:defence`/`track:defense`) labels. Without a track label, explicit incident
37
+ investigation wording suggests Defence; otherwise Software is the default.
38
+ Conflicting labels request a choice. This is a simple editable suggestion, not a
39
+ model assessment or permission to act. A security code fix can remain Software.
40
+ Both sources support either type; Defence still produces a private draft.
41
+
42
+ Repository templates are read from `.github/ISSUE_TEMPLATE` on the default
43
+ branch through GitHub's API. Markdown templates and YAML markdown, input,
44
+ textarea, dropdown (including multiple choices) and checkboxes are supported.
45
+ Required fields/defaults are preserved; compilation checks the template SHA
46
+ again and refuses a changed form. Unsupported forms (including uploads) stay
47
+ visible with a GitHub link instead of silently dropping fields. Contact links
48
+ preserve the project's private security reporting route. Template Markdown is
49
+ shown as literal text, never executed or rendered as raw HTML.
50
+
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
+
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):
67
+
68
+ ```sh
69
+ software-defence-factory issue connection --state PATH
70
+ software-defence-factory issue list --source remote --state PATH --page 1
71
+ software-defence-factory issue templates --state PATH
72
+ software-defence-factory issue draft --state PATH --template bug-report.yml --sha TEMPLATE_SHA --file answers.json > draft.json
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
79
+ ```
80
+
81
+ `answers.json` contains `{"title":"Fix the board","answers":{"problem":"..."}}`;
82
+ keys match `fields[].id` in `issue templates`. Multi-select/checkbox answers are
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.
37
101
 
38
102
  ## Change the definition
39
103
 
@@ -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: { supported: false, items: [] },
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,15 @@
1
+ // Suggestions never authorize execution or replace the operator's chosen scope.
2
+ export function recommendWork({ spec, labels = [] }) {
3
+ if (typeof spec !== 'string' || !spec.trim() || Buffer.byteLength(spec) > 240000) throw new Error('Provide a brief between 1 and 240 KB.');
4
+ if (!Array.isArray(labels) || labels.length > 100 || labels.some(label => typeof label !== 'string' || label.length > 100)) throw new Error('Expected issue label names.');
5
+ const names = labels.map(label => label.toLowerCase());
6
+ const software = names.includes('track:software');
7
+ const defence = names.some(label => ['track:security', 'track:defence', 'track:defense'].includes(label));
8
+ if (software !== defence) return { workflow: defence ? 'defence' : 'software', basis: 'label', reason: `The issue is labelled for ${defence ? 'security investigation' : 'software delivery'}.` };
9
+ if (software && defence) return { workflow: 'software', basis: 'conflicting_labels', reason: 'Both tracks are labelled. Choose whether this task should deliver code or investigate evidence.' };
10
+ // Deliberately narrow: fixing a vulnerability or building security features is software work.
11
+ if (/\b(?:investigat(?:e|ion|ing)|triage|analys[ei][rs]?|analy[sz]e|undersøg|undersøge)\b[\s\S]{0,100}\b(?:incident|intrusion|breach|malware|compromise|suspicious|security logs|hændelse|angreb)\b/i.test(spec)) {
12
+ return { workflow: 'defence', basis: 'brief', reason: 'The brief describes investigating a security incident or supplied evidence.' };
13
+ }
14
+ return { workflow: 'software', basis: 'default', reason: 'Software is the default for changes to this project. Choose Defence for a scoped security investigation.' };
15
+ }
@@ -1,22 +1,49 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import { readProjectLinks } from './project-links.mjs';
4
+ import { recommendWork } from './intake.mjs';
4
5
  const exec = promisify(execFile);
5
6
 
7
+ export async function githubRead(args) {
8
+ try {
9
+ const { stdout } = await exec('gh', args, {
10
+ encoding: 'utf8', timeout: 10000, maxBuffer: 8 * 1024 * 1024,
11
+ env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_PAGER: 'cat' },
12
+ });
13
+ return JSON.parse(stdout);
14
+ } catch (cause) {
15
+ const error = new Error('Could not read GitHub repository data. Check GitHub CLI access on the controller host and try again.');
16
+ if (/\(HTTP 404\)/.test(cause.stderr || '')) error.status=404;
17
+ throw error;
18
+ }
19
+ }
20
+
21
+ function issueLabels(labels = []) {
22
+ if (!Array.isArray(labels) || labels.some(label => typeof label?.name !== 'string')) throw new Error('GitHub returned unexpected labels.');
23
+ return labels.map(label => ({name:label.name, color:/^[a-f0-9]{6}$/i.test(label.color || '') ? label.color.toLowerCase() : null}));
24
+ }
25
+
26
+ export async function listIssues(repo, page = 1, read = githubRead) {
27
+ if (!Number.isSafeInteger(page) || page < 1 || page > 10000) throw new Error('Issue page must be an integer between 1 and 10000.');
28
+ const repository = readProjectLinks(repo)?.repository;
29
+ if (!repository) throw new Error('This project has no configured GitHub origin.');
30
+ const slug = repository.slice('https://github.com/'.length);
31
+ const result = await read(['api', '--hostname', 'github.com', `repos/${slug}/issues?state=open&sort=created&direction=desc&per_page=50&page=${page}`, '-H', 'Accept: application/vnd.github+json']);
32
+ if (!Array.isArray(result) || result.length > 50) throw new Error('GitHub returned an unexpected issue list.');
33
+ const issues = result.filter(issue => !issue.pull_request).map(issue => {
34
+ validateIssueURL(repository, issue.html_url);
35
+ 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) };
37
+ });
38
+ return { repository, issues, next_page: result.length === 50 && page < 10000 ? page + 1 : null };
39
+ }
40
+
6
41
  export function validateIssueURL(repoURL, value) {
7
42
  if (typeof value !== 'string' || value.length > 2048 || !/^https:\/\/github\.com\/[A-Za-z0-9-]+\/[A-Za-z0-9_.-]+\/issues\/[1-9][0-9]*$/.test(value)) throw new Error('Enter a GitHub issue URL without query parameters.');
8
43
  if (!repoURL || !value.toLowerCase().startsWith(`${repoURL.toLowerCase()}/issues/`)) throw new Error('Issue does not belong to this project’s configured GitHub origin.');
9
44
  return value;
10
45
  }
11
- export async function readIssue(repo, url, read = async url => {
12
- try {
13
- const { stdout } = await exec('gh', ['issue', 'view', url, '--json', 'title,body,url'], {
14
- encoding: 'utf8', timeout: 10000, maxBuffer: 300000,
15
- env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_PAGER: 'cat' },
16
- });
17
- return JSON.parse(stdout);
18
- } catch { throw new Error('Could not read the issue. Check the URL and GitHub CLI access on the controller host.'); }
19
- }) {
46
+ export async function readIssue(repo, url, read = url => githubRead(['issue', 'view', url, '--json', 'title,body,url,labels'])) {
20
47
  const repoURL = readProjectLinks(repo)?.repository;
21
48
  validateIssueURL(repoURL, url);
22
49
  const issue = await read(url);
@@ -24,5 +51,6 @@ export async function readIssue(repo, url, read = async url => {
24
51
  if (issue.url.toLowerCase() !== url.toLowerCase() || typeof issue.title !== 'string' || !issue.title.trim() || typeof issue.body !== 'string') throw new Error('GitHub returned an unexpected issue.');
25
52
  const spec = `Issue: ${issue.url}\n${issue.title}\n\n${issue.body}`;
26
53
  if (Buffer.byteLength(spec) > 240000) throw new Error('Issue exceeds the 240 KB task limit. Use a bounded task file instead.');
27
- return { title: issue.title, url: issue.url, body: issue.body, spec };
54
+ const labels = issueLabels(issue.labels);
55
+ return { title: issue.title, url: issue.url, body: issue.body, spec, labels, recommendation: recommendWork({ spec, labels: labels.map(label => label.name) }) };
28
56
  }
@@ -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
+ }