nx_nxtask_main.nx source
↩ module page · 279 lines · 10487 B
1// nxtask_main.nx -- CLI driver for the sovereign agent task graph.
2//
3// Usage:
4// nxtask demo build + dump sample graph as JSON
5// nxtask ready build + dump ready-task count
6// nxtask show N build + show task N's JSON
7// nxtask save <path> serialize demo graph to <path>
8// nxtask load <path> deserialize <path> + dump as JSON
9// nxtask roundtrip <path> save+load e2e smoke test
10//
11// Once multi-agent merge + durable event log ship (see
12// docs/NXTASK_AGENT_TASK_GRAPH.md v0.1.0), the CLI will manage a
13// default store under ~/.nxtask/ so the <path> argument becomes
14// optional. Today every subcommand takes an explicit path.
15//
16// When Mormon v0 picks this up, the shape becomes:
17//
18// run("nxtask load ~/.nxtask/graph.bin | jq '.tasks[] | select(.state==\"open\")'")
19// let next_task = parse_json(...).id
20// ...do the work...
21// run("nxtask close " + next_task.id + " 'summary' && nxtask save ~/.nxtask/graph.bin")
22//
23// Exit codes:
24// 0 success
25// 1 no args
26// 2 unknown subcommand / missing arg
27// 3 I/O failure (open/read/write)
28// 4 malformed input (deserialize failure)
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
34// verdict: NOT_YET_EVALUATED
35
36import "nx_syscalls.nx"
37import "nx_json_emit.nx"
38import "nx_nxtask.nx"
39import "nx_nxtask_store.nx"
40
41// === JSON serialisation =============================================
42//
43// Emit a task graph as a JSON object: { "tasks": [ { ... }, ... ] }
44
45const STATE_OPEN: i64 = 0
46const STATE_CLAIMED: i64 = 1
47const STATE_CLOSED: i64 = 2
48
49func state_name(s: i64) -> *u8 {
50 if s == STATE_OPEN { return "open" as *u8 }
51 if s == STATE_CLAIMED { return "claimed" as *u8 }
52 if s == STATE_CLOSED { return "closed" as *u8 }
53 return "unknown" as *u8
54}
55
56func state_name_len(s: i64) -> i64 {
57 if s == STATE_OPEN { return 4 }
58 if s == STATE_CLAIMED { return 7 }
59 if s == STATE_CLOSED { return 6 }
60 return 7
61}
62
63func emit_task(w: *JsonWriter, t: *Task, idx: i64) -> i64 {
64 json_begin_object(w)
65 json_emit_key(w, "idx" as *u8, 3)
66 json_emit_int(w, idx)
67 json_emit_key(w, "state" as *u8, 5)
68 json_emit_string(w, state_name(t.state), state_name_len(t.state))
69 json_emit_key(w, "desc" as *u8, 4)
70 json_emit_string(w, t.desc, t.desc_len)
71 if t.claimed_by != (0 as *u8) {
72 json_emit_key(w, "claimed_by" as *u8, 10)
73 json_emit_string(w, t.claimed_by, t.claimed_by_len)
74 }
75 if t.summary != (0 as *u8) {
76 json_emit_key(w, "summary" as *u8, 7)
77 json_emit_string(w, t.summary, t.summary_len)
78 }
79 json_emit_key(w, "n_blocked" as *u8, 9)
80 json_emit_int(w, t.n_blocked)
81 json_emit_key(w, "created_ns" as *u8, 10)
82 json_emit_int(w, t.created_ns)
83 json_emit_key(w, "updated_ns" as *u8, 10)
84 json_emit_int(w, t.updated_ns)
85 json_end_object(w)
86 return 0
87}
88
89func emit_graph(w: *JsonWriter, g: *TaskGraph) -> i64 {
90 json_begin_object(w)
91 json_emit_key(w, "tasks" as *u8, 5)
92 json_begin_array(w)
93 var i: i64 = 0
94 while i < g.n_tasks {
95 let t: *Task = task_at(g, i)
96 emit_task(w, t, i)
97 i = i + 1
98 }
99 json_end_array(w)
100 json_end_object(w)
101 return 0
102}
103
104// === demo graph ====================================================
105
106// Build a representative overnight-build graph so CLI has content
107// to emit without persistence. Mirrors the real overnight
108// workflow: big work items with cross-dependencies.
109func build_demo_graph() -> *TaskGraph {
110 let g: *TaskGraph = nxtask_graph_new(32)
111 nxtask_add(g, "Port regalloc to runtime/regalloc.nx" as *u8, 37, 1)
112 nxtask_add(g, "Port riscv 12-bit imm expansion to runtime" as *u8, 42, 2)
113 nxtask_add(g, "Wire F-extension lowering in runtime/riscv.nx" as *u8, 46, 3)
114 nxtask_add(g, "Add SIMD RVV intrinsic recognition" as *u8, 34, 4)
115 nxtask_add(g, "Build Mormon v0 with llama.cpp + corpus" as *u8, 39, 5)
116
117 // Dependencies: SIMD needs F-ext first; Mormon needs Phase 2.4.
118 nxtask_add_dep(g, 3, 2) // SIMD blocked by F-ext
119 nxtask_add_dep(g, 4, 3) // Mormon blocked by SIMD (via runtime speed)
120
121 // Simulate mid-workflow state.
122 nxtask_claim(g, 0, "mormon-v0" as *u8, 9, 10)
123 nxtask_close(g, 0, "Rematerialisation + cross-block liveness ported" as *u8, 47, 11)
124 return g
125}
126
127// === simple argv helpers ============================================
128
129// streq(a, b, n) -- true if the first n bytes match + both have a
130// NUL at n (to avoid prefix-matching "list" against "listxyz").
131func streq(a: *u8, b: *u8, n: i64) -> i64 {
132 var i: i64 = 0
133 while i < n {
134 if a[i] != b[i] { return 0 }
135 i = i + 1
136 }
137 if a[n] != 0 { return 0 }
138 if b[n] != 0 { return 0 }
139 return 1
140}
141
142func write_out(o: *u8, n: i64) -> i64 {
143 return sys_write(1, o, n)
144}
145
146// === main ==========================================================
147
148func main(argc: i64, argv: *i64) -> i64 {
149 if argc < 2 {
150 write_out("nxtask: usage: nxtask <demo|ready|show N>\n" as *u8, 42)
151 return __syscall(93, 1, 0, 0, 0, 0, 0)
152 }
153 let cmd: *u8 = argv[1] as *u8
154
155 let g: *TaskGraph = build_demo_graph()
156 let buf: *u8 = sys_mmap(65536)
157 let w_raw: *u8 = sys_mmap(128)
158 let w: *JsonWriter = w_raw as *JsonWriter
159 json_writer_init(w, buf, 65536)
160
161 if streq(cmd, "demo" as *u8, 4) == 1 {
162 emit_graph(w, g)
163 write_out(buf, w.pos)
164 write_out("\n" as *u8, 1)
165 return __syscall(93, 0, 0, 0, 0, 0, 0)
166 }
167 if streq(cmd, "ready" as *u8, 5) == 1 {
168 let nxt: i64 = nxtask_next_ready(g)
169 if nxt < 0 {
170 write_out("{\"ready\":null}\n" as *u8, 15)
171 } else {
172 let t: *Task = task_at(g, nxt)
173 json_begin_object(w)
174 json_emit_key(w, "ready" as *u8, 5)
175 emit_task(w, t, nxt)
176 json_end_object(w)
177 write_out(buf, w.pos)
178 write_out("\n" as *u8, 1)
179 }
180 return __syscall(93, 0, 0, 0, 0, 0, 0)
181 }
182 if streq(cmd, "show" as *u8, 4) == 1 {
183 if argc < 3 {
184 write_out("nxtask show: need task idx\n" as *u8, 28)
185 return __syscall(93, 2, 0, 0, 0, 0, 0)
186 }
187 // For v0.0.1 parse a single digit only (argv-index parsing
188 // is a separate commit). Usable for demo graph with idx < 10.
189 let idx_str: *u8 = argv[2] as *u8
190 let idx: i64 = idx_str[0] - 0x30
191 if idx < 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
192 if idx >= g.n_tasks { return __syscall(93, 2, 0, 0, 0, 0, 0) }
193 let t: *Task = task_at(g, idx)
194 emit_task(w, t, idx)
195 write_out(buf, w.pos)
196 write_out("\n" as *u8, 1)
197 return __syscall(93, 0, 0, 0, 0, 0, 0)
198 }
199 // ---- persistence subcommands -------------------------------------
200 if streq(cmd, "save" as *u8, 4) == 1 {
201 if argc < 3 {
202 write_out("nxtask save: need path\n" as *u8, 24)
203 return __syscall(93, 2, 0, 0, 0, 0, 0)
204 }
205 let path: *u8 = argv[2] as *u8
206 let store_buf: *u8 = sys_mmap(1048576) // 1 MB headroom
207 let n: i64 = nxtask_serialize(g, store_buf, 1048576)
208 if n < 0 {
209 write_out("nxtask save: serialise overflow\n" as *u8, 33)
210 return __syscall(93, 3, 0, 0, 0, 0, 0)
211 }
212 let fd: i64 = sys_openat_wr(path, 0x1B6) // 0666
213 if fd < 0 {
214 write_out("nxtask save: open failed\n" as *u8, 26)
215 return __syscall(93, 3, 0, 0, 0, 0, 0)
216 }
217 let wrote: i64 = sys_write(fd, store_buf, n)
218 sys_close(fd)
219 if wrote != n {
220 write_out("nxtask save: short write\n" as *u8, 26)
221 return __syscall(93, 3, 0, 0, 0, 0, 0)
222 }
223 // Brief ack so the shell sees success without needing exit code.
224 write_out("{\"saved\":true}\n" as *u8, 15)
225 return __syscall(93, 0, 0, 0, 0, 0, 0)
226 }
227 if streq(cmd, "load" as *u8, 4) == 1 {
228 if argc < 3 {
229 write_out("nxtask load: need path\n" as *u8, 24)
230 return __syscall(93, 2, 0, 0, 0, 0, 0)
231 }
232 let path: *u8 = argv[2] as *u8
233 let len_slot_raw: *u8 = sys_mmap(16)
234 let len_slot: *i64 = len_slot_raw as *i64
235 let file_buf: *u8 = sys_read_file(path, len_slot)
236 if file_buf == (0 as *u8) {
237 write_out("nxtask load: read failed\n" as *u8, 26)
238 return __syscall(93, 3, 0, 0, 0, 0, 0)
239 }
240 let n: i64 = *len_slot
241 let g_loaded: *TaskGraph = nxtask_deserialize(file_buf, n)
242 if g_loaded == (0 as *TaskGraph) {
243 write_out("nxtask load: malformed\n" as *u8, 24)
244 return __syscall(93, 4, 0, 0, 0, 0, 0)
245 }
246 emit_graph(w, g_loaded)
247 write_out(buf, w.pos)
248 write_out("\n" as *u8, 1)
249 return __syscall(93, 0, 0, 0, 0, 0, 0)
250 }
251 if streq(cmd, "roundtrip" as *u8, 9) == 1 {
252 // End-to-end smoke test: save + load + assert n_tasks matches.
253 // Returns exit 0 on success, 3 on I/O failure, 4 on mismatch.
254 if argc < 3 {
255 write_out("nxtask roundtrip: need path\n" as *u8, 29)
256 return __syscall(93, 2, 0, 0, 0, 0, 0)
257 }
258 let path: *u8 = argv[2] as *u8
259 let store_buf: *u8 = sys_mmap(1048576)
260 let n1: i64 = nxtask_serialize(g, store_buf, 1048576)
261 if n1 < 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
262 let fd: i64 = sys_openat_wr(path, 0x1B6)
263 if fd < 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
264 sys_write(fd, store_buf, n1)
265 sys_close(fd)
266 let len_slot_raw: *u8 = sys_mmap(16)
267 let len_slot: *i64 = len_slot_raw as *i64
268 let file_buf: *u8 = sys_read_file(path, len_slot)
269 if file_buf == (0 as *u8) { return __syscall(93, 3, 0, 0, 0, 0, 0) }
270 let g_loaded: *TaskGraph = nxtask_deserialize(file_buf, *len_slot)
271 if g_loaded == (0 as *TaskGraph) { return __syscall(93, 4, 0, 0, 0, 0, 0) }
272 if g_loaded.n_tasks != g.n_tasks { return __syscall(93, 4, 0, 0, 0, 0, 0) }
273 write_out("{\"roundtrip\":\"ok\"}\n" as *u8, 19)
274 return __syscall(93, 0, 0, 0, 0, 0, 0)
275 }
276
277 write_out("nxtask: unknown subcommand\n" as *u8, 27)
278 return __syscall(93, 2, 0, 0, 0, 0, 0)
279}